/
Теги: компьютерные технологии операционные системы операционная система linux
ISBN: 978-5-94157-957-0
Год: 2007
Текст
Understanding the
LINUX
KERNEL
Understanding the
LINUX
KERNEL
THIRD EDITION
Daniel P. Bovet and Marco Cesati
O'REILLY*
Beijing • Cambridge • Farnham • Koln • Paris • Sebastopol • Taipei • Tokyo
Д. Бовет, М. Чезати
Ядро
LINUX
3-е издание
Санкт-Петербург
«БХВ-Петербург»
2007
УДК 681.3.06
ББК 32.973.26-018.2
Б72
Бовет, Д.
Б72 Ядро Linux, 3-е изд.: Пер. с англ. / Бовет Д., Чезати М. — СПб.:
БХВ-Петербург, 2007. — 1104 с: ил.
ISBN 978-5-94157-957-0
В книге обсуждается большинство структур данных, алгоритмы и приемы
программирования, применяемые в ядре, излагается подробная информация
о строении современной операционной системы. Рассматривается управление
памятью, в том числе буферизация файлов, выгрузка процессов и прямой дос-
доступ к памяти (DMA); виртуальная файловая система, Ext2 и Ext3, создание про-
процессов и планирование их выполнения, сигналы, прерывания и важнейшие ин-
интерфейсы драйверов устройств, хронометрирование, синхронизация внутри яд-
ядра, межпроцессорное взаимодействие (IPC), выполнение программ.
Приводится построчный комментарий соответствующих фрагментов кода
Отсканированно для forum.ru-board.com
Материал книги базируется на версии ядра 2.6.
Для системных администраторов и программистов
УДК 681.3.06
ББК 32.973.26-018.2
Группа подготовки издания:
Главный редактор Екатерина Кондукова
Зам. главного редактора Татьяна Лапина
Зав. редакцией Григорий Добин
Перевод с английского Сергея Иноземцева
Научный редактор Валентин Синицын
Редактор Елена Кашлакова
Компьютерная верстка Ольги Сергиенко
Корректор Зинаида Дмитриева
Оформление обложки Инны Тачиной
Зав. производством Николай Тверских
Authorized translation of the English edition of Understanding the Linux Kernel, Third Edition, Copyright © 2006
O'Reilly Media, Inc. All rights reserved. This translation is published and sold by permission of 2006 O'Reilly Media,
Inc., the owner of all rights to publish and sell the same.
Авторизованный перевод английской редакции, выпущенной O'Reilly Media, Inc., © 2006. Все права защищены.
Перевод опубликован и продается с разрешения O'Reilly Media, Inc., собственника всех прав на публикацию и
продажу издания.
Лицензия ИД № 02429 от 24.07.00. Подписано в печать 11.07.07.
Формат 70x1001/i6. Печать офсетная. Усл. печ. л. 89,01.
Тираж 3000 экз. Заказ № 2227.
"БХВ-Петербург", 194354, Санкт-Петербург, ул. Есенина, 5Б.
Санитарно-эпидемиологическое заключение на продукцию
№ 77.99.02.953.Д.006421.11.04 от 11.11.2004 г. выдано Федеральной службой
по надзору в сфере защиты прав потребителей и благополучия человека.
Отпечатано по технологии CtP
в ОАО «Печатный двор» им. А. М. Горького
197110, Санкт-Петербург, Чкаловский пр., 15.
ISBN 0-596-00565-2 (анГЛ.) © 2006 O'Reilly Media, Inc.
ISBN 978-5-94157-957-0 (pVC ) ® Перевод на русский язык "БХВ-Петербург", 2007
Оглавление
Об авторах 19
Предисловие 21
Наша читательская аудитория 21
Организация материала 22
Уровень описания 23
Обзор книги 24
На кого рассчитана эта книга 26
Соглашения, принятые в книге 26
Благодарности 26
Глава 1. Введение 29
Сравнение Linux с другими Unix-подобными ядрами 30
Монолитное ядро 31
Откомпилированные и статически скомпонованные традиционные ядра Unix... 32
Потоки ядра 32
Поддержка многопоточных приложений 32
Вытеснение в ядре 32
Поддержка многопроцессорной обработки 33
Файловая система 33
STREAMS 33
Зависимость от оборудования 35
Версии Linux 36
Основные понятия операционных систем 37
Многопользовательские системы 39
Пользователи и группы 39
Процессы 40
Архитектура ядра 42
Обзор файловой системы Unix 43
Файлы 43
Жесткие и гибкие ссылки 45
Типы файлов 45
Дескриптор файла и индексный дескриптор 46
Права доступа и режим файла 47
Системные вызовы для работы с файлами 48
Обзор ядер Unix 51
Модель процесс/ядро 51
Реализация процесса 53
Реентерабельные ядра 53
Адресное пространство процесса 55
Синхронизация и критические области 56
Сигналы и взаимодействие между процессами 60
Управление процессами 61
Управление памятью 63
Драйверы устройств 67
Глава 2. Адресация памяти 71
Адреса памяти 71
Сегментация в аппаратной части 73
Селектор сегментов и сегментные регистры 73
Дескрипторы сегментов 74
Быстрый доступ к дескрипторам сегментов 76
Блок сегментации 77
Сегментация в Linux 78
Глобальная таблица дескрипторов Linux 80
Локальные таблицы дескрипторов Linux 82
Управление страницами на аппаратном уровне 83
Обычное управление страницами 84
Расширенное управление страницами 87
Аппаратная схема защиты 88
Пример обычного управления страницами 88
Механизм расширения физических адресов (РАЕ) 90
Управление страницами в 64-разрядных архитектурах 92
Аппаратный кэш 93
Буферы быстрого преобразования адреса (TLB) 96
Управление страницами в Linux 96
Поля линейного адреса 99
Работа с Таблицей Страниц 100
Схема разбивки физической памяти 107
Таблицы страниц процесса 111
Таблицы страниц ядра 111
Фиксированно отображенные линейные адреса 116
Работа с аппаратным кэшем и TLB-буфером 118
Глава 3. Процессы 123
Процессы, облегченные процессы и потоки 123
Дескриптор процесса 125
Состояние процесса 126
Идентификация процесса 128
Взаимоотношения между процессами 136
Магическая константа 139
Как организованы процессы 143
Ограничения на ресурсы процесса 149
Переключение процессов 151
Аппаратный контекст 151
Сегмент состояния задачи 152
Выполнение переключения процессов 154
Сохранение и загрузка регистров FPU, ММХ и ХММ 161
Создание процессов 166
Системные вызовы cloneQ,fork() и vforkQ 166
Потоки ядра 177
Уничтожение процессов 180
Завершение процесса 180
Удаление процессов 183
Глава 4. Прерывания и исключения 187
Роль сигналов прерываний 188
Прерывания и исключения 189
Прерывания 189
Исключения 189
IRQ и прерывания 191
Исключения 195
Таблица дескрипторов прерываний 198
Аппаратная обработка прерываний и исключений 199
Вложенное выполнение обработчиков исключений и прерываний 202
Инициализация таблицы дескрипторов прерываний 204
Шлюзы прерываний, ловушек и системы 204
Предварительная инициализация таблицы IDT 206
Обработка исключений 207
Сохранение регистров для обработчика исключений 209
Вход и выход из обработчика исключений 210
Обработка прерываний 211
Обработка прерываний ввода/вывода 212
Обработка межпроцессорных прерываний 235
Softirq-функции и тасклеты 236
Softirq-функции 238
Тасклеты 245
Рабочие очереди 247
Структуры данных для рабочих очередей 248
Возврат из прерываний и исключений 251
Точки входа 254
Возобновление управляющего тракта ядра 255
Проверка вытеснения в ядре 255
Возобновление программы режима пользователя 256
Проверка необходимости перепланирования 256
Обработка висящих сигналов, режима virtual-8086 и пошагового режима 257
Глава 5. Синхронизация в ядре 259
Как ядро обслуживает запросы 259
Вытеснение в ядре 260
Когда синхронизация необходима 263
Когда в синхронизации нет необходимости 264
Примитивы синхронизации 265
Процессорные переменные 266
Атомарные операции 267
Барьеры оптимизации и барьеры памяти 270
Спин-блокировки 272
Спин-блокировки чтения/записи 277
Seqlock-блокировки 280
Обновление копии для чтения (RCU) 282
Семафоры 284
Семафоры чтения/записи 289
Completion-блокировки 290
Отключение локальных прерываний 291
Запрет и разрешение функций отложенного выполнения 292
Синхронизация обращений к структурам данных ядра 293
Выбор между спин-блокировками, семафорами и отключением прерываний... 295
Примеры предотвращения конфликтов 301
Счетчики ссылок 301
Глобальная блокировка ядра 301
Семафор чтения/записи для дескриптора памяти 304
Семафор для списка slab-кэшей 304
Семафор для индексного дескриптора 304
Глава 6. Хронометраж 307
Микросхемы часов и таймеров 308
Часы реального времени 308
Счетчик отметок времени 309
Программируемый таймер интервалов 309
Локальный таймер процессора 311
Высокоточный таймер событий 312
ACPI-таймер управления питанием 313
Архитектура хронометрирования в Linux 313
Структуры данных в архитектуре хронометрирования 314
Архитектура хронометрирования в однопроцессорных системах 318
Архитектура хронометрирования в многопроцессорных системах 321
Обновление времени и даты 323
Обновление системной статистики 324
Обновление статистики локального процессора 324
Отслеживание нагрузки на систему 325
Профилирование кода ядра 326
Проверка сторожей NMI Watchdog 327
Программные таймеры и функции задержки 327
Динамические таймеры 328
Применение динамических таймеров: системный вызов nanosleepQ 335
Функции задержки 337
Системные вызовы, относящиеся к хронометрированию 338
Системные вызовы timeQ и gettimeofdayQ 338
Системный вызов adjtimexQ 340
Системные вызовы setitimerQ иа1агт() 341
Системные вызовы для таймеров POSIX 342
Глава 7. Планирование процессов 345
Политика планирования 345
Вытеснение процессов 348
Сколько должен длиться квант времени? 349
Алгоритм планирования 350
Планирование обычных процессов 351
Планирование процессов реального времени 355
Структуры данных, используемые планировщиком 356
Структура runqueue 356
Дескриптор процесса 359
Функции, вызываемые планировщиком 361
Функция scheduler JickQ 361
Функция try to wake up0 365
Функция recalc_task_prio() 367
Функция scheduleQ 369
Балансирование очередей на выполнение в многопроцессорных системах 378
Области планирования 380
Функция rebalanceJickQ 382
Функция loadbalanceQ 383
Функция movetasksQ 384
Системные вызовы, относящиеся к планированию 385
Системный вызов niceQ 385
Системные вызовы getpriorityQ и setpriorityQ 386
Системные вызовы sched_getaffinityQ и sched_setqffinity() 387
Системные вызовы, относящиеся к процессам реального времени 388
Глава 8. Управление памятью 391
Управление страничными кадрами 391
Дескрипторы страниц 392
Доступ к неоднородной памяти (NUMA) 395
Зоны памяти 397
Пул зарезервированных страничных кадров 401
Зонный аллокатор страничных кадров 402
Отображение ядром страничных кадров верхней памяти 405
Алгоритм "buddy-система" 413
Кэш страничных кадров процессора 419
Аллокатор зон 422
Управление областями памяти 427
Slab-аллокатор 427
Дескриптор кэша 429
Дескриптор участка памяти 431
Общие и специальные кэши 432
Интерфейс между slab-аллокатором и зонным аллокатором страничных
кадров 434
Выделение участка памяти кэшу 436
Освобождение участка памяти из кэша 437
Дескриптор объекта 438
Выравнивание объектов в памяти 439
Окрашивание участков памяти 439
Локальные кэши свободных объектов-участков памяти 441
Выделение объекта в участке 443
Освобождение объекта в участке 445
Объекты общего назначения 447
Пулы памяти 448
Управление несмежными областями памяти 450
Линейные адреса несмежных областей памяти 451
Дескрипторы несмежных областей памяти 451
Выделение несмежной области памяти 453
Освобождение несмежной области памяти 457
Глава 9. Адресное пространство процесса 461
Адресное пространство процесса 462
Дескриптор памяти 464
Дескриптор памяти для потоков ядра 468
Области памяти 470
Структуры данных для областей памяти 472
Права доступа к области памяти 475
Работа с областями памяти 479
Выделение интервала линейных адресов 484
Освобождение интервала линейных адресов 488
Обработчик исключения "ошибка обращения к странице" 493
Обработка ошибочного адреса, не входящего в адресное пространство 499
Обработка ошибочного адреса, входящего в адресное пространство 500
Выделение страниц по требованию 503
Копирование при записи 507
Обработка обращений к несмежным областям памяти 510
Создание и удаление адресного пространства процесса 512
Создание адресного пространства процесса 512
Удаление адресного пространства процесса 514
Управление кучей 515
Глава 10. Системные вызовы 519
API-интерфейсы стандарта POSIX и системные вызовы 519
Обработчик системного вызова и служебные процедуры 521
Вход в системный вызов и выход из него 523
Выполнение системного вызова с помощью инструкции int $0x80 523
Выполнение системного вызова с помощью инструкции sysenter 526
Передача параметров 532
Проверка параметров 534
Доступ к адресному пространству процесса 536
Динамическая проверка адресов: код обработки исключения 538
Таблицы исключений 539
Генерирование таблицы исключений и кода обработки 540
Интерфейсные процедуры ядра 543
Глава 11. Сигналы 545
Роль сигналов 545
Действия, выполняемые при доставке сигнала 550
Сигналы POSIX и многопоточные приложения 551
Структуры данных, ассоциированные с сигналами 552
Операции над сигнальными структурами 558
Генерирование сигнала 560
Функция specific_send_sig_infoQ 561
Функция send_signalQ 562
Функция group_send_sig_infoQ 564
Доставка сигнала 567
Выполнение действия по умолчанию по обработке сигнала 569
Обработка сигнала 571
Повторное выполнение системных вызовов 576
Системные вызовы, связанные с обработкой сигналов 580
Системный вызов killQ 580
Системные вызовы tkillQ mtghillQ 581
Изменение действия сигнала 582
Просмотр висящих заблокированных сигналов 583
Модификация набора заблокированных сигналов 584
Приостановка выполнения процесса 585
Системные вызовы для сигналов реального времени 586
Глава 12. Виртуальная файловая система 587
Роль виртуальной файловой системы (VFS) 587
Общая файловая модель 590
Системные вызовы, обрабатываемые VFS 593
Структуры данных VFS 594
Суперблоки 595
Индексный дескриптор 600
Файловые объекты 605
Элемент каталога 610
Кэш элементов каталога 613
Файлы, связанные с процессом 615
Типы файловых систем 618
Специальные файловые системы 618
Регистрация типа файловой системы 620
Работа с файловой системой 622
Пространства имен 622
Монтирование файловых систем 623
Монтирование типичной файловой системы 627
Монтирование корневой файловой системы 632
Размонтирование файловой системы 635
Анализ пути 636
Стандартный анализ пути 640
Анализ пути к родительскому каталогу 645
Анализ символьных ссылок 647
Реализация системных вызовов VFS 649
Системный вызов openQ 649
Системные вызовы readQ и writeQ 652
Системный вызов closeQ 654
Блокировка файлов 654
Блокировка файлов в Linux 656
Структуры данных для блокировок файлов 657
Блокировки FLFLOCK 659
Блокировки FLPOSIX 662
Глава 13. Архитектура ввода/вывода и драйверы устройств 667
Архитектура ввода/вывода 667
Порты ввода/вывода 669
Интерфейсы ввода/вывода 672
Контроллеры устройств 674
Модель драйвера устройства 675
Файловая система sysfs 676
Объекты kobject 677
Компоненты модели драйвера устройства 681
Файлы устройств 687
Работа с файлами устройств в режиме пользователя 689
Работа с файлами устройств в VFS 691
Драйверы устройств 692
Регистрация драйвера устройства 693
Инициализация драйвера устройства 694
Мониторинг операций ввода/вывода 694
Обращение к совместно используемой памяти ввода/вывода 698
Прямой доступ к памяти (DMA) 700
Уровни поддержки ядра 705
Драйверы символьных устройств 707
Присваивание номеров устройств 709
Обращение к драйверу символьного устройства 712
Стратегии буферизации для символьных устройств 713
Глава 14. Драйверы блочных устройств 715
Управление блочными устройствами 716
Секторы 719
Блоки 720
Сегменты 721
Общий слой работы с блочными устройствами 722
Структура Ыо 723
Представление дисков и разделов на диске 725
Выдача запроса 728
Планировщик ввода/вывода 730
Дескрипторы очередей запросов 731
Дескриптор запросов 734
Активизация драйвера блочного устройства 739
Алгоритмы планирования ввода/вывода 740
Выдача запроса планировщику ввода/вывода 744
Драйверы блочных устройств 747
Блочные устройства 747
Регистрация и инициализация драйвера устройства 751
Процедура-стратег 755
Обработчик прерываний 758
Открытие файла блочного устройства 759
Глава 15. Кэш страниц 763
Кэш страниц 764
Объект address_spасе 765
Базисное дерево 769
Функции для работы с кэшем страниц 772
Теги базисного дерева 776
Хранение блоков в кэше страниц 778
Буферы блоков и головы буферов 779
Работа с головами буферов 780
Страницы буферов 781
Выделение страниц буферов блочных устройств 783
Освобождение страниц буферов блочных устройств 785
Поиск блоков в кэше страниц 785
Передача голов буферов общему слою работы с блочными устройствами 788
Запись грязных страниц на диск 791
Потоки ядра pdflush 792
Поиск грязных страниц для записи на диск 794
Запись старых грязных страниц на диск 799
Системные вызовы syncQ.fsyncO nfdatasyncQ 800
Системный вызов syncQ 800
Системные вызовы fsyncO ufdatasyncQ 801
Глава 16. Работа с файлами 803
Чтение и запись файла 804
Чтение из файла 805
Опережающее чтение файлов 817
Запись в файл 824
Запись грязных страниц на диск 831
Отображение в память 834
Структуры отображения в память 835
Создание отображения в память 837
Уничтожение отображения в память 840
Выделение страниц по требованию для отображения в память 840
Принудительная запись на диск грязных страниц отображения в память 844
Нелинейные отображения в память 845
Прямой ввод/вывод 847
Асинхронный ввод/вывод 850
Асинхронный ввод/вывод в Linux 2.6 852
Глава 17. Утилизация страничных кадров 857
Алгоритм утилизации страничных кадров 857
Выбор целевой страницы 858
Описание алгоритма PFRA 861
Обратное отображение 863
Обратное отображение для анонимных страниц 865
Обратное отображение для отображающих страниц 870
Реализация алгоритма PFRA 874
Списки давно неиспользуемых страниц (LRU) 876
Утилизация при дефиците памяти 884
Утилизация страниц сокращаемых кэшей диска 893
Периодическая утилизация 896
Уничтожение процессов из-за нехватки памяти 899
Жетон защиты от выгрузки 901
Подкачка 902
Область подкачки 904
Дескриптор области подкачки 906
Идентификатор выгруженной страницы 909
Перевод области подкачки в активное и неактивное состояние 911
Выделение и освобождение страничного слота 919
Кэш подкачки 922
Выгрузка страниц 926
Загрузка выгруженных страниц 929
Глава 18. Файловые системы Ext2 и Ext3 935
Общие характеристики файловой системы Ext2 936
Структуры Ext2 на диске 939
Суперблок 940
Дескриптор группы и битовая карта 943
Таблица индексных дескрипторов 944
Расширенные атрибуты индексного дескриптора 946
Списки управления доступом 947
Как файлы разных типов используют блоки на диске 948
Структуры Ext2 в памяти 950
Объект-суперблок Ext2 951
Индексный дескриптор Ext2 953
Создание файловой системы Ext2 954
Методы Ext2 956
Операции суперблока Ext2 956
Операции индексного дескриптора Ext2 956
Файловые операции Ext2 958
Управление пространством на дискевЕх!2 959
Создание индексных дескрипторов 960
Удаление индексных дескрипторов 963
Адресация блоков данных 964
Дыры в файлах 967
Выделение блоков данных 968
Освобождение блока данных 969
Файловая система Ext3 970
Журналируемые файловые системы 971
Ext3 —журналируемая файловая система 971
Слой журналирующего блочного устройства 973
Как ведется журнал 977
Глава 19. Взаимодействие процессов 981
Каналы 982
Работа с каналом 983
Структуры данных для канала 985
Создание и уничтожение канала 989
Чтение из канала 990
Запись в канал 993
FIFO-файлы 995
Создание и открытие FIFO-файлов 996
Схема межпроцессного взаимодействия System V IPC 999
Работа с ресурсом IPC 999
Системный вызов ipc() 1003
Семафоры IPC 1004
Сообщения IPC 1009
Совместно используемая память IPC 1013
Очереди сообщений POSIX 1018
Глава 20. Выполнение программ 1021
Исполняемые файлы 1022
Права и способности процесса 1023
Аргументы командной строки и окружение оболочки 1030
Библиотеки 1031
Сегменты программы и области памяти процесса 1033
Отслеживание выполнения 1038
Форматы исполняемых файлов 1041
Области выполнения 1043
Функции exec 1045
Приложение 1. Запуск системы 1053
Доисторические времена: BIOS 1053
Античность: загрузчик 1055
Загрузка Linux с диска 1056
Средние века: функция setupQ 1057
Эпоха Возрождения: функции startup_32Q 1058
Новейшее время: функция start_kernelQ 1060
Приложение 2. Модули 1061
Быть (модулем) или не быть? 1061
Лицензии на модули 1062
Реализация модулей 1063
Счетчики обращений 1066
Экспортирование символов 1067
Зависимости модулей 1067
Подключение и выгрузка модулей 1068
Подключение модулей по требованию 1071
Программа modprobe 1071
Функция requestjnoduleQ 1072
Предметный указатель 1073
Об авторах
Даниэль Бове (Daniel P. Bovet) получил степень доктора компьютерных наук
в Калифорнийском университете в Лос-Анджелесе в 1968 г. и является про-
профессором университета Tor Vergata в Риме. Ему пришлось ждать 25 лет, пре-
прежде чем он смог вести курс "Операционные системы" так, как ему хотелось,
поскольку все эти годы не был доступен исходный код современных хорошо
спроектированных систем. Теперь, благодаря недорогим персональным ком-
компьютерам и ОС Linux Даниэль и Марко могут объяснить студентам все тон-
тонкости операционных систем и предложить им трудные, но интересные до-
домашние задания. (Теперешняя молодежь избалована — у студентов дома сто-
стоят персональные компьютеры, и им никогда не приходилось возиться с
колодами перфокарт.) Даниэль был так поражен достижениями Линуса Тор-
вальдса (Linus Torvalds) и его последователей, что в течение последних не-
нескольких лет пытался разобраться в тонкостях Linux. Естественно, что, про-
проделав такую работу, он написал книгу о том, что узнал.
Марко Чезати (Marco Cesati) получил диплом математика в 1992 г. и степень
доктора компьютерных наук в университете La Sapienza в Риме, в 1995 г.
Сейчас он работает научным сотрудником на факультете компьютерных тех-
технологий в School of Engineering при университете Tor Vergata в Риме. Ранее
он работал системным администратором и программистом Unix в универси-
университете (когда был аспирантом) и консультантом в различных организациях.
В течение последних лет он учит своих студентов, как можно модифициро-
модифицировать ядро Linux необычным и даже забавным образом.
Предисловие
В весеннем семестре 1997 г. мы преподавали курс "Операционные системы",
в основе которого лежал Linux 2.0. Нам хотелось стимулировать студентов
читать исходный код. Чтобы добиться этого, мы предложили им курсовые
работы, в которых требовалось внести изменения в ядро и протестировать
модифицированную версию. Кроме того, мы написали несколько коммента-
комментариев к лекциям для наших студентов, где рассматривали некоторые критиче-
критические функции Linux, такие как переключение процессов и их планирование.
Из этой работы — при огромной поддержке со стороны редактора издатель-
издательства O'Reilly Энди Орама (Andy Oram) — и выросло первое издание книги
"Understanding the Linux Kernel", увидевшее свет в 2000 г. Оно было посвя-
посвящено Linux 2.2 и некоторым ожидаемым возможностям Linux 2.4. Успех этой
книги вдохновил нас на продолжение работы в этом направлении. В конце
2002 г. вышло второе издание с описанием Linux 2.4. Вы держите в руках
третье издание, в котором говорится о Linux 2.6.
При работе над предыдущими изданиями мы изучили тысячи строк кода, пы-
пытаясь разобраться в нем. Теперь мы можем сказать, что наши усилия не про-
пропали напрасно. Мы узнали много такого, чего вы не найдете в печатных из-
изданиях, и мы надеемся, что нам удалось передать часть этой информации на
страницах данной книги.
Наша читательская аудитория
Все, кому интересно узнать, как работает операционная система Linux и по-
почему она так эффективна, найдут здесь ответы на эти вопросы. Прочитав
книгу, вы научитесь ориентироваться в тысячах строк кода, отличая важные
структуры данных от второстепенных— короче, станете настоящим Linux-
хакером.
Нашу работу можно считать путеводителем по ядру Linux; здесь обсуждается
большинство структур данных, а также многие алгоритмы и приемы про-
программирования, применяемые в ядре. Во многих случаях приводится по-
построчный комментарий соответствующих фрагментов кода. Конечно, читате-
читателю следует иметь под рукой исходный код Linux и испытывать желание ра-
разобраться в некоторых функциях, описанных не очень подробно, ради
экономии места.
Книга имеет и другой уровень, на котором излагается информация для тех,
кто хочет больше узнать о строении современной операционной системы.
Она адресована не только системным администраторам и программистам, но,
в первую очередь, тем, кто хочет понять, что же в действительности происхо-
происходит внутри компьютера! Как и во всяком хорошем путеводителе, мы не огра-
ограничиваемся поверхностным обзором. Мы предлагаем читателю дополнитель-
дополнительную информацию, например, историю развития важнейших функциональных
возможностей и причины их появления.
Организация материала
Когда мы только приступали к работе над этой книгой, нам пришлось при-
принимать принципиальное решение. Следует ли нам придерживаться какой-то
конкретной аппаратной платформы, или лучше пропустить аппаратные под-
подробности и сосредоточиться исключительно на аппаратно-независимых ком-
компонентах ядра?
В некоторых книгах по внутреннему строению ядра Linux выбран второй
подход, но мы решили придерживаться первого, и вот по каким причинам:
□ эффективные ядра пользуются большинством доступных функциональных
возможностей аппаратуры, например, способами адресации, кэшами, ис-
исключениями, генерируемыми процессором, специальными инструкциями,
управляющими регистрами процессора, и т. д. Если мы хотим убедить чи-
читателя, что ядро действительно великолепно справляется с какой-то зада-
задачей, мы должны сначала объяснить, какая поддержка исходит со стороны
оборудования;
□ хотя значительная порция исходного кода ядра Unix действительно не за-
зависит от архитектуры и написана на языке С, небольшая и очень важная
часть закодирована на ассемблере. Следовательно, глубокое знание ядра
невозможно без изучения ассемблерных фрагментов, взаимодействующих
с аппаратной частью.
При описании аппаратных особенностей мы придерживались простой страте-
стратегии: бегло описывать те, что полностью управляются аппаратурой, и детали-
зировать возможности, требующие программной поддержки. В конце концов,
нас интересует строение ядра, а не архитектура компьютеров.
Следующий шаг на нашем пути заключался в выборе платформы, к которой
бы мы привязали описание. Хотя Linux в настоящее время работает на самых
разных персональных компьютерах и рабочих станциях, мы решили остано-
остановиться на очень популярных и дешевых IBM-совместимых персональных
компьютерах, а значит, на микропроцессорах 80x86 и некоторых вспомога-
вспомогательных чипах, входящих в состав этих персональных компьютеров. Термин
микропроцессор 80x86 используется в этой книге для обозначения микро-
микропроцессоров Intel 80386, 80486, Pentium, Pentium Pro, Pentium II, Pentium III и
Pentium 4 или совместимых модулей. В некоторых случаях делаются прямые
ссылки на конкретные модели.
Мы должны были принять еще одно решение: выбрать порядок изучения
компонентов Linux. Выбор сделан в пользу подхода "снизу вверх". Мы начи-
начинаем с вопросов, тесно связанных с аппаратной частью и заканчиваем пол-
полностью аппаратно-независимыми. Например, в первой части книги мы посто-
постоянно упоминаем о микропроцессорах 80x86, а темы остальных глав в мень-
меньшей степени привязаны к оборудованию. Исключением являются главы 13 и
14. Следовать подходу "снизу вверх" на практике не так просто, как кажется.
Такие вопросы, как управление памятью, управление процессами и работа
файловых систем, тесно переплетены, и ссылки вперед, т. е. на темы, еще не
обсуждавшиеся в книге, просто неизбежны.
Каждая глава начинается с теоретического обзора вопросов, которые она за-
затрагивает. Материал представлен в порядке "снизу вверх". Мы начинаем со
структур данных, необходимых для поддержания функциональных возмож-
возможностей, описанных в главе. Затем мы, как правило, двигаемся от функций
низшего уровня на более высокие уровни и часто заканчиваем главу демон-
демонстрацией того, как поддерживаются системные вызовы, выдаваемые пользо-
пользовательскими приложениями.
Уровень описания
Исходный код Linux для всех поддерживаемых архитектур составляет более
14 000 файлов на языке С и ассемблере и хранится приблизительно в тыся-
тысяче подкаталогов. Он состоит из примерно 6 млн строк, которые занимают
свыше 230 Мбайт на диске. Конечно, эта книга может покрыть лишь малую
часть этого кода. Чтобы читатель представил себе, насколько велик код
Linux, заметим, что текст этой книги занимает менее 3 Мбайт. Получается,
что на листинг всего кода уйдет более 75 таких книг, причем без наших ком-
комментариев!
Поэтому нам пришлось выбирать, какие части кода описывать. Вот беглый
перечень наших решений:
□ мы довольно подробно описываем управление памятью и управление про-
процессами;
□ мы обсуждаем виртуальную файловую систему и файловые системы Ext2
и Ext3? хотя многие функции лишь упомянуты без обсуждения их кода;
другие файловые системы, поддерживаемые в Linux, не обсуждаются;
□ мы описываем драйверы устройств, занимающие примерно 50% ядра, в
той степени, в какой это касается интерфейса ядра; мы не пытаемся анали-
анализировать каждый конкретный драйвер.
Материал этой книги относится к официальной версии 2.6.11 ядра Linux, ко-
которая может быть загружена с сайта http://www.kernel.org.
Читатель должен отдавать себе отчет, что во многих дистрибутивах
GNU/Linux официальный код ядра модифицирован с целью реализации
новых функциональных возможностей, повышающих эффективность ядра.
В отдельных случаях исходный код, имеющийся на вашем дистрибутиве,
может значительно отличаться от кода, описываемого в книге.
Часто мы приводим фрагменты оригинального кода, переписанные так, что-
чтобы их было легче читать, но с понижением эффективности. Это относится к
точкам, критическим в отношении времени работы. Программы в таких мес-
местах обычно написаны на смеси ассемблера и вручную оптимизированного
кода С. Повторимся, что наша цель — помочь читателю разобраться в ориги-
оригинальном коде Linux.
Обсуждая код ядра, мы часто переходим к описанию скрытой стороны мно-
многих известных функциональных возможностей Unix, о которых программи-
программисты наслышаны и, возможно, хотели бы узнать побольше (совместно исполь-
используемая и отображаемая память, сигналы, каналы, символьные ссылки и т. д.).
Обзор книги
Чтобы облегчить дальнейшее чтение, в главе 7, ''Введение", представлена об-
общая картина того, что находится внутри ядра Unix, и показано, как Linux
конкурирует с другими известными системами семейства Unix.
Важнейшей частью любого ядра Unix является управление памятью. В гла-
главе 2, "Адресация памяти", разъясняется, как процессоры 80x86 используют
специальные встроенные устройства для адресации данных в памяти и как
Linux эксплуатирует их.
Процессы— фундаментальная абстракция, предлагаемая системой Linux.
Они представлены в главе 3, "Процессы". Здесь мы разъясняем, как каждый
процесс работает либо в непривилегированном пользовательском режиме,
либо в привилегированном режиме ядра. Переключение из режима пользова-
пользователя в режим ядра и обратно происходит только с помощью хорошо опреде-
определенных аппаратных механизмов, называемых прерываниями и исключения-
исключениями. Они представлены в главе 4, "Прерывания и исключения".
Нередко ядру приходится иметь дело со всплеском сигналов прерывания, по-
поступающих от различных устройств и процессоров. Чтобы ядро могло пооче-
поочередно обслужить все эти запросы, необходимы механизмы синхронизации.
Они обсуждаются в главе 5, Синхронизация в ядре", причем рассматривают-
рассматриваются как однопроцессорные, так и многопроцессорные системы.
Один тип прерываний очень важен для Linux, поскольку позволяет отслежи-
отслеживать интервалы времени. Подробности читатель найдет в главе 6, "Хроно-
"Хронометраж:".
В главе 7, "Планирование процессов", разъясняется, как Linux поочередно вы-
выполняет все активные процессы в системе, так что они постепенно приходят
к своему завершению.
Затем мы опять займемся памятью. В главе 8, "Управление памятью", описы-
описываются сложные технические приемы работы с самым ценным (после про-
процессоров, естественно) ресурсом системы — с памятью. Этот ресурс нужен
как ядру Linux, так и пользовательским приложениям. В главе 9, "Адресное
пространство процесса", показано, как ядро справляется с запросами на па-
память, выдаваемыми "жадными" приложениями.
В главе 10, "Системные вызовы", разъясняется, как процесс, работающий в
режиме пользователя, передает запросы ядру, а в главе 11, "Сигналы"— как
он отправляет синхронизирующие сигналы другим процессам. После этого
мы готовы перейти к еще одной, исключительно важной теме: к реализации
файловой системы в Linux. Этому вопросу посвящен целый ряд глав. В гла-
главе 12, "Виртуальная файловая система", представлен общий слой, поддер-
поддерживающий много разных файловых систем. Некоторые файлы Linux являют-
являются особыми, потому что представляют собой "люки", ведущие к аппаратным
устройствам. Глава 13, "Архитектура ввода/вывода и драйверы устройств",
и глава 14, "Драйверы блочных устройств", позволяют читателю больше
узнать об этих специальных файлах и соответствующих драйверах аппарат-
аппаратных устройств.
Еще одним вопросом, подлежащим обсуждению, является время обращения к
диску. В главе 15, "Кэш страниц", показано, как разумное использование
оперативной памяти сокращает количество обращений к дискам, тем самым
значительно повышая производительность системы. Опираясь на материал
этих глав, мы сможем в главе 16, "Работа с файлами", объяснить, как поль-
пользовательские приложения работают с обычными файлами. Глава 17, "Утили-
зация страничных кадров", завершает обсуждение управления памятью в
Linux и описывает технические приемы, используемые в Linux для обеспече-
обеспечения достаточного количества памяти в любой момент времени. Последняя
глава, посвященная работе с файлами, — глава 18, "Файловые системы Ext2
и Ext3". В ней рассматривается файловая система, наиболее широко исполь-
используемая в Linux, а именно Ext2, и результат ее развития, файловая система
Ext3.
Последние две главы завершают наше путешествие по ядру Linux. В главе 19,
"Взаимодействие процессов", представлены механизмы взаимодействия, дос-
доступные процессам в режиме пользователя, отличные от сигналов. В главе 20,
"Выполнение программ", поясняется, как запускаются пользовательские про-
программы.
Последним по порядку, но не по важности, являются приложения. Приложе-
Приложение 1, "Запуск системы", представляет набросок того, как загружается систе-
система Linux, а приложение 2, "Модули", описывает, как можно динамически ме-
менять конфигурацию ядра, добавляя и удаляя функциональность, по мере не-
необходимости.
На кого рассчитана эта книга
От читателя не требуются никаких предварительных знаний, если не считать
определенных навыков программирования на языке С и, возможно, некото-
некоторых представлений об ассемблере.
Соглашения, принятые в книге
Мы придерживаемся определенных полиграфических соглашений:
моноширинный шрифт — используется для представления содержимого файлов
с кодом и результатов работы команд, а также для обозначения ключевых
слов, встречающихся в коде.
Курсив — служит для выделения новых терминов.
Благодарности
Эта книга не появилась бы на свет без участия многих студентов университе-
университета Tor Vergata в Риме, которые слушали наш курс и пытались разобраться в
комментариях к лекциям по ядру Linux. Их энергичное стремление понять
исходный код заставило нас переработать текст и исправить многие ошибки.
Огромной благодарности заслуживает Энди Орам (Andy Oram), замечатель-
замечательный редактор издательства O'Reilly Media. Он был первым из сотрудников
издательства, кто поверил в наш проект и потратил массу времени и энергии
на разбор наших черновых вариантов рукописи. Он также внес много пред-
предложений по улучшению текста книги и написал вводные разделы к несколь-
нескольким главам.
У нас было несколько квалифицированных рецензентов, внимательно прочи-
прочитавших текст. Первое издание проверили (в алфавитном порядке) Алан Кокс
(Alan Сох), Майкл Керриск (Michael Kerrisk), Пол Кинцельман (Paul
Kinzelman), Раф Левьен (Raph Levien) и Рик ван Риль (Rik van Riel).
Над вторым изданием поработали Эрез Задок (Erez Zadok), Джерри Купер-
штейн (Jerry Cooperstein), Джон Герцен (John Goerzen), Майкл Керриск
(Michael Kerrisk), Пол Кинцельман (Paul Kinzelman), Рик ван Риль (Rik van
Riel) и Уолт Смит (Walt Smith).
Рецензентами этого издания были Чарльз П. Райт (Charles P. Wright), Клемент
Бучачер (Clemens Buchacher), Эрез Задок (Erez Zadok), Рафаэль Финкель
(Raphael Finkel), Рик ван Риль (Rik van Riel) и Роберт П. Дж. Дэй (Robert P. J.
Day). Их замечания, а также комментарии многочисленных читателей из раз-
разных мест земного шара помогли нам убрать несколько ошибок и неточностей
и улучшить текст этой книги.
Даниэль Бове (Daniel P. Bovet),
Марко Чезати (Marco Cesati),
Июль, 2005 г.
ГЛАВА 1
Введение
Linux1 является членом большого семейства Unix-подобных операционных
систем. Новичок, на которого в конце 90-х годов неожиданно обрушилась
популярность у пользователей, Linux стоит в одном ряду с хорошо известны-
известными коммерческими Unix-подобными операционными системами, такими как
System V Release 4 (SVR4), разработанная компанией AT&T (теперь принад-
принадлежащей группе SCO Group); BSD версии 4.4 из Калифорнийского универси-
университета в Беркли D.4BSD); Digital UNIX из Digital Equipment Corporation (теперь
Hewlett-Packard); AIX от IBM; HP-UX от Hewlett-Packard; Solaris от Sun
Microsystems и Mac OS X от Apple Computer, Inc. Кроме Linux, существуют и
другие ядра Unix-подобных систем с открытым кодом, такие как FreeBSD,
NetBSD и OpenBSD.
Операционная система Linux была разработана Линусом Торвальдсом (Linus
Torvalds) в 1991 г. для IBM-совместимых персональных компьютеров на базе
микропроцессора Intel 80386. Л. Торвальдс по-прежнему занимается улучше-
улучшением системы Linux, сопровождением ее в соответствии с разнообразными
аппаратными новшествами и координированием деятельности сотен разра-
разработчиков Linux по всему миру. Со временем разработчики сделали систему
Linux доступной на других архитектурах, в том числе Alpha от Hewlett-
Packard, Itanium от Intel, AMD64 от AMD, PowerPC и zSeries от IBM.
Одной из наиболее привлекательных черт систем Linux является ее неком-
некоммерческий статус. По условиям лицензии GNU GPL (General Public LicenseJ
1 LINUX® является зарегистрированным товарным знаком, принадлежащим Линусу Торвальдсу.
2 Проект GNU координируется организацией Free Software Foundation, Inc. (http://www.gnu.org).
Его цель— реализация полноценной операционной системы, свободно предоставляемой любому
желающему. Существование компилятора GNU С явилось одним из значимых факторов успеха
проекта Linux.
исходный код Linux является открытым и доступен любому для изучения
(которое и является предметом этой книги). Если вы загрузите этот код
(с официального сайта http://www.kernel.org) или откроете исходные файлы
на компакт-диске с Linux, вы сможете "от корки до корки" исследовать одну
из самых успешных операционных систем. Во всяком случае, мы предпола-
предполагаем, что у читателя есть под рукой исходный код, и он сможет использовать
в своих исследованиях текст этой книги.
В техническом смысле Linux является настоящим ядром Unix, хотя и не
представляет собой полную операционную систему Unix, поскольку не
включает в себя все приложения Unix, такие как утилиты для работы с фай-
файловой системой, оконные системы и графические рабочие столы, команды
для системного администрирования, текстовые редакторы, компиляторы
и т. д. Однако, поскольку большинство этих программ находится в свободном
доступе по условиям GPL, их можно установить в любой системе на базе
Linux.
Поскольку ядро Linux требует установки большого количества дополнитель-
дополнительного программного обеспечения для создания полезного окружения, многие
предпочитают приобретать код стандартной системы Unix в составе коммер-
коммерческих3 дистрибутивов. В качестве альтернативы код можно загрузить с не-
нескольких сайтов, например, http://www.kernel.org. В некоторых дистрибути-
дистрибутивах исходный код Linux помещен в каталог /usr/src/linux. Далее в этой книге
все пути к файлам неявно ссылаются на каталог с исходным кодом Linux.
Сравнение Linux
с другими Unix-подобными ядрами
Unix-подобные системы, доступные на рынке, многие из которых имеют дол-
долгую историю и демонстрируют морально устаревшие подходы, различаются
во многих важных аспектах. Все коммерческие варианты построены на осно-
основе либо SVR4, либо 4.4BSD, и, как правило, согласуются с некоторыми об-
общепринятыми стандартами, например, со стандартом POSIX (Portable
Operating Systems based on Unix, Переносимые операционные системы на ос-
основе Unix), принятом IEEE, или с САЕ (Common Applications Environment,
Общая среда приложений) от Х/Ореп.
Действующие стандарты определяют только программный интерфейс для
приложений (API), т. е. среду, в которой пользовательские программы долж-
3 Коммерческие дистрибутивы Linux не обязательно являются платными. Как правило, их также
можно свободно загружать из Интернета.
ны работать. Таким образом, стандарты не накладывают ограничений на
внутренние конструктивные особенности ядра4.
Чтобы определить общий пользовательский интерфейс, Unix-подобные ядра
часто придерживаются одних и тех же базовых конструктивных идей и име-
имеют одинаковые функциональные возможности. В этом отношении система
Linux сравнима с другими Unix-подобными операционными системами. По-
Поэтому чтение этой книги и изучение ядра Linux поможет вам разобраться в
других вариантах Unix.
Версия 2.6 ядра Linux стремится удовлетворять стандарту IEEE POSIX. Есте-
Естественно, это означает, что большинство существующих программ под Unix
может быть откомпилировано и выполнено в системе Linux без дополнитель-
дополнительных усилий и даже без "заплат" в исходном коде. Более того, Linux имеет все
характерные черты современной системы Unix: виртуальную память, вирту-
виртуальную файловую систему, облегченные процессы, сигналы Unix, взаимодей-
взаимодействие между процессами SVR4, поддержку симметричной многопроцессор-
многопроцессорной обработки (SMP) и т. д.
Когда Л. Торвальдс создавал код первого ядра, он опирался на некоторые
классические работы по внутреннему устройству Unix, например, на книгу
"The Design of the Unix Operating System" Мориса Баха (Maurice Bach), выпу-
выпущенную издательством Prentice Hall в 1986 г. По правде говоря, Linux до сих
пор демонстрирует некоторую склонность к базовому варианту, описанному
в книге Баха (то есть SVR2). Однако никакой конкретный вариант Unix не
лежит в основе Linux. Наоборот, эта операционная система старается вопло-
воплотить лучшие функциональные возможности и конструктивные решения не-
нескольких различных ядер Unix.
Покажем, как Linux конкурирует с некоторыми хорошо известными коммер-
коммерческими ядрами Unix.
Монолитное ядро
Это большая и сложная программа в духе "делай-все-сам", состоящая из не-
нескольких логически различающихся компонентов. В этом смысле она не вы-
выделяется на фоне коммерческих вариантов Unix, большинство которых тоже
монолитны. (Известными исключениями являются операционные системы
Apple Mac OS X и GNU Hurd, являющиеся производными от системы Mach
из университета Карнеги-Меллона, которая следует микроядерному под-
подходу.)
4 Интересно, что некоторые операционные системы, не входящие в семейство Unix, например
Windows NT и ее потомки, совместимы со стандартом POSIX.
Откомпилированные и статически скомпонованные
традиционные ядра Unix
Большинство современных ядер способно динамически загружать и выгру-
выгружать отдельные порции своего кода (как правило, драйверы устройств),
обычно называемые модулями. Поддержка модулей в Linux очень хороша,
поскольку Linux может автоматически загружать и выгружать модули по ме-
мере необходимости. Из коммерческих вариантов Unix только ядра SVR4.2 и
Solaris обладают аналогичной функциональностью.
Потоки ядра
Некоторые ядра Unix, например Solaris и SVR4.2/MP, организованы в виде
набора потоков ядра. Поток ядра— это контекст выполнения, который до-
допускает независимое планирование. Он может быть ассоциирован с пользо-
пользовательской программой, а может выполнять лишь некоторые функции ядра.
Переключение контекстов между потоками ядра обычно обходится дешевле,
чем между обычными процессами, потому что первые обычно работают в
общем адресном пространстве. В Linux потоки ядра применяются весьма ог-
ограниченно, для периодического выполнения некоторых функций ядра. При
этом они не представляют базовую абстракцию контекста выполнения. Это
тема следующего раздела.
Поддержка многопоточных приложений
Большинство современных операционных систем реализует определенную
поддержку многопоточных приложений, т. е. пользовательских приложений,
разработанных в виде нескольких относительно независимых ветвей выпол-
выполнения, совместно использующих значительную часть данных приложения.
Многопоточное пользовательское приложение может состоять из нескольких
облегченных процессов, т. е. процессов, которые могут работать в общем ад-
адресном пространстве, с общими страницами физической памяти, общими от-
открытыми файлами и т. д. Linux определяет собственную версию облегченных
процессов, отличную от тех, что используются в других системах, таких как
SVR4 и Solaris. В то время как во всех коммерческих вариантах Unix облег-
облегченные процессы имеют в своей основе потоки ядра, Linux считает облегчен-
облегченные процессы базовым контекстом выполнения и работает с ними с помощью
нестандартного системного вызова clone ().
Вытеснение в ядре
Будучи откомпилированным с опцией "Preemptible Kernel" (вытеснение в яд-
ядре), ядро Linux 2.6 может произвольным образом чередовать ветви выполне-
ния, когда они находятся в привилегированном режиме. Кроме Linux 2.6, не-
несколько других распространенных неспециализированных систем Unix, на-
например Solaris и Mach 3.0, обладают полностью вытесняющими ядрами.
В системе SVR4.2/MP имеются фиксированные точки вытеснения, позво-
позволяющие реализовать ограниченное вытеснение.
Поддержка многопроцессорной обработки
Некоторые варианты ядра Unix поддерживают многопроцессорные системы.
Linux 2.6 поддерживает симметричную многопроцессорную обработку для
различных моделей памяти, включая NUMA: системы могут включать в себя
несколько процессоров, и каждый процессор может выполнять любое зада-
задание, т. е. они равноправны. Хотя некоторые участки кода ядра по-прежнему
сериализуются с помощью "глобальной блокировки ядра", справедливость
требует признать, что поддержка многопроцессорной обработки в Linux 2.6
почти идеальна.
Файловая система
Стандартные файловые системы Linux имеют много вариантов. Можно вос-
воспользоваться "старой доброй" файловой системой Ext2, если у вас нет каких-
то специфических потребностей. Чтобы избежать длительных проверок фай-
файловой системы после сбоев, можно переключиться на Ext3. Если вы собирае-
собираетесь работать с большим количеством маленьких файлов, то лучшим выбором
будет файловая система ReiserFS. Помимо Ext3 и ReiserFS, в Linux сущест-
существуют и другие журналируемые файловые системы, например Journaling File
System (JFS) от IBM AIX и XFS от Silicon Graphics IRIX. Благодаря мощной
объектно-ориентированной технологии виртуальной файловой системы
(вдохновителями создания которой были Solaris и SVR4), перенос "чужих"
файловых систем на Linux выполняется гораздо проще, чем на другие ядра.
STREAMS
В Linux отсутствует аналог подсистемы ввода/вывода STREAMS, впервые
появившейся в SVR4, хотя в настоящее время эта подсистема включена в
большинство ядер Unix и является предпочтительным интерфейсом при реа-
реализации драйверов устройств, терминальных драйверов и сетевых прото-
протоколов.
Эта сравнительная оценка показывает, что сегодня Linux успешно конкури-
конкурирует с коммерческими операционными системами. Более того, ряд функцио-
функциональных возможностей Linux делает его особенно привлекательным. Ком-
Коммерческие ядра Unix часто предлагают пользователям новые функциональ-
ные возможности, чтобы отхватить сектор рынка побольше, однако эти воз-
возможности не всегда полезны, стабильны и продуктивны. На самом деле, со-
современные ядра Unix, как правило, излишне "раздуты". В противоположность
им, Linux (как и другие операционные системы с открытым исходным кодом)
не зависит от состояния рынка и может свободно развиваться в соответствии
с идеями его разработчиков (в основном, Л. Торвальдса). В частности, Linux
обладает следующими преимуществами перед своими конкурентами:
□ Linux — бесплатная система. Вы можете установить у себя полноценную
систему Unix совершенно бесплатно (естественно, если не считать расхо-
расходов на оборудование).
□ Linux— полностью настраиваемая система во всех своих компонентах.
Благодаря наличию опций компиляции вы можете настраивать ядро, вы-
выбирая только действительно необходимые функциональные возможности.
Кроме того, GPL позволяет вам свободно читать и модифицировать ис-
исходный код ядра и всех системных программ5.
□ Linux работает на старых, недорогих аппаратных платформах. Вы можете
построить сетевой сервер на базе системы Intel 80386 с 4 Мбайт оператив-
оперативной памяти.
□ Linux — мощная операционная система. Системы Linux очень производи-
производительны, потому что полностью эксплуатируют возможности аппаратуры.
Основной целью Linux является эффективность, и многие конструктивные
решения, принятые в коммерческих вариантах, например подсистема вво-
ввода/вывода STREAMS, были отвергнуты разработчиками Linux, как пони-
понижающие производительность системы.
□ Разработчики Linux — высокопрофессиональные программисты. Системы
Linux весьма стабильны; они имеют очень низкий процент сбоев и требу-
требуют мало времени на обслуживание.
□ Ядро Linux может быть небольшим и очень компактным. Образ ядра вме-
вместе с некоторыми системными программами можно уместить на дискете
емкостью 1,44 Мбайт. Насколько нам известно, ни один коммерческий ва-
вариант Unix не может быть загружен с одной дискеты.
Linux обладает прекрасной совместимостью со многими распространенными
операционными системами. Linux позволяет вам напрямую монтировать
файловые системы для всех версий MS-DOS и Microsoft Windows, SVR4,
OS/2, Mac OS X, Solaris, SunOS, NEXTSTEP, многих вариантов BSD и т. д.
5 Многие коммерческие фирмы поддерживают работу своих продуктов в операционной системе
Linux. Однако эти продукты, как правило, не распространяются по открытой лицензии, так что вам
не разрешается читать и модифицировать их исходный код.
Кроме того, Linux может работать со многими сетевыми технологиями, таки-
такими как Ethernet (а также Fast Ethernet, Gigabit Ethernet и 10 Gigabit Ethernet),
Fiber Distributed Data Interface (FDDI), High Performance Parallel Interface
(HIPPI), IEEE 802.11 (Wireless LAN) и IEEE 802.15 (Bluetooth). С помощью
соответствующих библиотек системы Linux могут выполнять программы,
написанные для других операционных систем. Например, в Linux можно вы-
выполнять некоторые приложения, созданные для MS-DOS, Microsoft Windows,
SVR3 и R4, 4.4BSD, SCO Unix, Xenix и других операционных систем на
платформе 80x86.
Linux хорошо сопровождается. Хотите — верьте, хотите — нет, но получить
заплатки и обновления для Linux гораздо проще, чем для иной закрытой
операционной системы. Ответ на ваш вопрос часто приходит в течение не-
нескольких часов после отправки сообщения в какую-нибудь сетевую конфе-
конференцию или список рассылки. Кроме того, драйверы для Linux обычно дос-
доступны уже через несколько недель после появления на рынке новых аппарат-
аппаратных устройств. В противоположность этому, производители аппаратных
устройств создают драйверы лишь для немногих коммерческих операцион-
операционных систем, как правило, разработанных в Microsoft. В результате все ком-
коммерческие варианты Unix работают с ограниченным набором аппаратных
компонентов.
По некоторым оценкам операционная система Linux установлена на несколь-
нескольких десятках миллионов компьютеров, и пользователи, привыкшие к опреде-
определенному набору функциональных возможностей, стандартных в других сис-
системах, ожидают того же и от Linux. В связи с этим спрос на разработчиков
Linux постоянно растет. К счастью, Linux развивается под внимательным ру-
руководством Л. Торвальдса и нескольких лиц, ответственных за его подсисте-
подсистемы, так что потребности массового пользователя удовлетворяются.
Зависимость от оборудования
Разработчики Linux стараются проводить четкое различие между аппаратно-
зависимым и аппаратно-независимым исходным кодом. По этой причине ка-
каталоги arch и include включают в себя 23 подкаталога, которые соответствуют
различным аппаратным платформам, поддерживаемым системой Linux. Эти
платформы имеют следующие стандартные названия:
□ alpha— рабочие станции Alpha от Hewlett-Packard (изначально Digital,
затем Compaq, в настоящее время не выпускаются);
□ arm, arm26 — компьютеры на базе процессоров ARM, например, карман-
карманные компьютеры и встроенные устройства;
□ cris — процессоры с "сокращенным набором инструкций", применяемые
компанией Axis в "тонких" серверах, таких как Web-камеры и макетные
платы;
□ frv— встроенные системы на базе микропроцессоров семейства FR-V
производства Fujitsu;
О Ь8300— 8- и 16-разрядные RISC-микропроцессоры Ь8/300 и h8S произ-
производства Hitachi;
□ i386— IBM-совместимые персональные компьютеры на базе микропро-
микропроцессоров 80x86;
□ ia64 — рабочие станции на базе 64-разрядного микропроцессора Itanium
производства Intel;
□ m32r— компьютеры на базе микропроцессоров M32R производства
Renesas;
□ m68k, m68knommu — персональные компьютеры на базе микропроцессо-
микропроцессоров МС680х0 производства Motorola;
□ mips — рабочие станции на базе микропроцессоров MIPS, например, от
Silicon Graphics;
□ parisc — рабочие станции на базе микропроцессоров HP 9000 семейства
РА-RISC производства Hewlett-Packard;
□ ррс, ррс64 — рабочие станции на базе 32- и 64-разрядных микропроцессо-
микропроцессоров PowerPC производства Motorola-IBM;
□ s390 — мэйнфреймы IBM ESA/390 и zSeries;
□ sh, sh64 — встроенные системы на базе микропроцессоров SuperH, разра-
разработанных компаниями Hitachi и STMicroelectronics;
□ spare, sparc64— рабочие станции на базе микропроцессоров SPARC и
64-разрядного Ultra SPARC производства Sun Microsystems;
□ um — User Mode Linux, виртуальная платформа, позволяющая разработ-
разработчикам запускать ядро в режиме пользователя;
□ v850 — микроконтроллеры NEC V850, имеющие 32-разрядное ядро RISC-
процессора на базе архитектуры Harvard;
□ х86_64 — рабочие станции на базе 64-разрядных микропроцессоров про-
производства AMD, например Athlon и Opteron, а также 64-разрядных микро-
микропроцессоров ia32e/EM64T производства Intel.
Версии Linux
Вплоть до версии 2.5 идентификация ядер Linux происходила по простой ну-
нумерационной схеме. Каждая версия определялась тремя числами, разделен-
ными точками. Первые два числа идентифицировали версию, а третье — вы-
выпуск. Первое число в номере версии, а именно 2, оставалось неизменным с
1996 г. Второе число определяло тип ядра: если оно было четным, это озна-
означало, что версия стабильна, в противном случае версия находилась в состоя-
состоянии разработки.
Как понятно из названия, стабильные версии были тщательно проверены ди-
дистрибьюторами Linux и хакерами. Новая стабильная версия выпускалась
только для устранения ошибок и добавления новых драйверов устройств.
Версии в состоянии разработки значительно отличались друг от друга. Разра-
Разработчикам позволялось экспериментировать с решениями, что время от вре-
времени приводило к резким изменениям в коде ядра. Пользователи, выполняв-
выполнявшие приложения на таких версиях, могли столкнуться с неприятными сюр-
сюрпризами при переходе на новый выпуск ядра.
Однако при создании версии 2.6 ядра Linux схема нумерации версий была
существенно изменена. Главное — второе число больше не идентифицирует
стабильность версии. Как следствие, сейчас разработчики ядра вносят серь-
серьезные изменения в текущую версию 2.6. Новая ветка ядра 2.7 будет создана
только после того, как разработчикам придется протестировать действитель-
действительно принципиальное изменение. Эта ветка 2.7 приведет к появлению новой
текущей версии или будет влита в версию 2.6, или, в конечном счете, будет
отброшена как тупиковая.
Новый подход к разработке Linux подразумевает, что два ядра с одинаковы-
одинаковыми номерами версий, но разными номерами выпусков, например 2.6.10 и
2.6.11, могут значительно различаться даже в ключевых компонентах и фун-
фундаментальных алгоритмах. Поэтому, когда появляется новый выпуск ядра, он
потенциально не стабилен и содержит ошибки. Для решения этой проблемы
разработчики ядра могут выпустить "заплатанные" версии любого ядра, и в
схеме нумерации версий это будет отражено четвертым числом. Например,
на момент написания этих строк последней, так сказать, стабильной версией
ядра была 2.6.11.12.
Читателю необходимо принять во внимание, что ядро, описываемое в этой
книге, имеет версию 2.6.11.
Основные понятия операционных систем
Каждая компьютерная система включает в себя базовый набор программ,
называемый операционной системой. Самая важная программа в этом наборе
называется ядром. Она загружается в оперативную память на этапе началь-
начальной загрузки системы и содержит ряд важнейших процедур, необходимых
для функционирования системы. Другие программы являются менее критич-
критичными утилитами; они предоставляют пользователю широкий набор интер-
интерактивных функций, а также выполняют всю ту работу, ради которой пользо-
пользователь купил компьютер. И все-таки основная форма и возможности опреде-
определяются ядром. Ядро обеспечивает ключевыми средствами все остальные
компоненты системы и определяет многие характеристики программного
обеспечения на более высоких уровнях. По этой причине мы часто использу-
используем термин "операционная система" как синоним ядра.
Операционная система должна решать две основные задачи:
□ взаимодействовать с аппаратными компонентами, обслуживая низкоуров-
низкоуровневые программируемые элементы платформы;
□ предоставлять среду выполнения приложениям, работающим на компью-
компьютере (так называемым пользовательским программам).
Некоторые операционные системы позволяют пользовательским программам
напрямую работать с аппаратной частью (типичным примером является MS-
DOS). В отличие от них Unix-подобные операционные системы скрывают все
низкоуровневые детали физической организации компьютера от программ,
запускаемых пользователем. Когда программе требуется аппаратный ресурс,
она должна выдать запрос операционной системе. Ядро оценивает запрос и,
если принимает решение предоставить ресурс, вступает во взаимодействие
с аппаратными компонентами от имени пользовательской программы.
Для усиления этого механизма современные операционные системы опира-
опираются на специфические аппаратные характеристики, которые не позволяют
пользовательским программам непосредственно взаимодействовать с низко-
низкоуровневыми аппаратными компонентами или обращаться к произвольным
ячейкам памяти. В частности, аппаратная часть поддерживает, как минимум,
два режима работы процессора: непривилегированный режим для пользова-
пользовательских программ и привилегированный для ядра. В Unix они называются
режимом пользователя и режимом ядра соответственно.
Далее в этой главе мы представим базовые концепции системы Unix, кото-
которым ее разработчики следовали на протяжении последних двух десятилетий,
а также понятия, лежащие в основе Linux и других операционных систем.
Возможно, эти понятия знакомы читателю как пользователю Linux, но в по-
последующих разделах мы постараемся описать их чуть глубже, чем это обыч-
обычно делается, и объяснить, каким образом они определяют строение ядра опе-
операционной системы. Эти общие рассуждения справедливы в отношении
практически любой Unix-подобной операционной системы. В других главах
этой книги мы надеемся помочь вам понять внутреннее устройство ядра
Linux.
Многопользовательские системы
Многопользовательская система — это компьютер, способный параллельно
выполнять несколько независимых приложений, принадлежащих двум и бо-
более пользователям. Здесь "параллельно" означает, что приложения активны
одновременно и соперничают в борьбе за различные ресурсы, такие как про-
процессор, память, жесткие диски и т. д. "Независимые" означает, что каждое
приложение может решать свои задачи, не заботясь о том, чем занимаются
приложения других пользователей. Конечно, переключение с одного прило-
приложения на другое замедляет работу каждого из них и снижает их время откли-
отклика с точки зрения пользователя. Сложное устройство ядер у современных
операционных систем (которое и является предметом обсуждения этой кни-
книги) во многом объясняется стремлением разработчиков минимизировать за-
задержки в работе каждой программы и обеспечить максимально быструю ре-
реакцию пользовательских программ.
Многопользовательские операционные системы должны обладать следую-
следующими функциональными особенностями:
□ механизмом аутентификации для проверки личности пользователя;
□ механизмом защиты от пользовательских программ с ошибками, которые
могут мешать работе других программ в системе;
□ механизмом защиты от злонамеренных пользовательских программ, кото-
которые могут вмешаться в действия других пользователей или шпионить за
ними;
□ механизмом ограничения объема ресурсов, выделяемых каждому пользо-
пользователю.
Для обеспечения механизмов защиты операционные системы должны приме-
применять аппаратные методы защиты, ассоциированные с привилегированным
режимом работы процессора. В противном случае пользовательские про-
программы смогут напрямую обратиться к аппаратной части системы и нару-
нарушить установленные границы. Unix является многопользовательской систе-
системой, в которой используется аппаратная защита системных ресурсов.
Пользователи и группы
В многопользовательской системе у каждого пользователя есть "частные вла-
владения" на компьютере. Как правило, он обладает определенным пространст-
пространством на диске для хранения файлов, получает личные почтовые сообщения
и т. д. Операционная система должна гарантировать, что закрытая часть поль-
пользовательского пространства видна только его владельцу. В частности, она
должна гарантировать, что ни один пользователь не сможет эксплуатировать
вать системное приложение для вторжения в закрытое пространство другого
пользователя.
Каждый пользователь идентифицируется уникальным числом, которое
называется идентификатором пользователя, или UID (User ID). Обычно
лишь ограниченному количеству людей разрешено пользоваться данной ком-
компьютерной системой. Когда один из них открывает сеанс работы, система
просит его ввести имя пользователя и пароль. Если пользователь не вводит
правильную пару, система отказывает ему в доступе. Поскольку предполага-
предполагается, что пароль держится в секрете, защита частных прав пользователя обес-
обеспечена.
Чтобы выборочно владеть ресурсами совместно с другими пользователями,
каждый пользователь присоединяется к одной или нескольким группам поль-
пользователей, которые идентифицируются числами, называемыми идентифика-
идентификаторами групп пользователей. Каждый файл ассоциирован с одной и только
одной группой пользователей. Доступ к этому файлу может быть настроен,
например, так, что владелец этого файла имеет право на чтение и запись,
группа — только право на чтение, а для остальных пользователей системы
доступ к этому файлу будет закрыт.
В любой Unix-подобной операционной системе есть особый пользователь,
называемый root, или суперпользователь. Системный администратор должен
войти в систему как суперпользователь, чтобы иметь возможность работать с
учетными записями пользователей, выполнять задачи по обслуживанию сис-
системы (например, создание резервных копий и обновление программ) и т. д.
Суперпользователь может делать почти все, потому что операционная систе-
система не применяет к нему обычные механизмы защиты. В частности, супер-
суперпользователь имеет доступ к любому файлу в системе и может манипулиро-
манипулировать любой работающей пользовательской программой.
Процессы
Во всех операционных системах имеется некая фундаментальная абстрак-
абстракция— процесс. Процесс можно определить как "экземпляр выполняемой
программы" или как "контекст выполнения" работающей программы. В тра-
традиционных системах процесс выполняет одну последовательность инструк-
инструкций в адресном пространстве, представляющем собой множество адресов
памяти, к которым процессу разрешено обращаться. В современных операци-
операционных системах допускается существование процессов с несколькими ветвя-
ветвями выполнения, т. е. в одном адресном пространстве может выполняться не-
несколько последовательностей инструкций.
Многопользовательские системы должны обеспечить среду выполнения,
в которой несколько процессов могут быть активными одновременно, конку-
рируя за системные ресурсы, в основном процессор. Системы, допускающие
наличие параллельных активных процессов, называются многопрограммны-
многопрограммными или многозадачными6. Важно отличать программы от процессов: несколь-
несколько процессов могут параллельно выполнять одну программу, и в то же время
один процесс может последовательно выполнить несколько программ.
В однопроцессорных системах только один процесс может получить в свое
распоряжение процессор, и, следовательно, в данный момент времени может
выполняться только одна последовательность инструкций. Вообще говоря,
количество процессоров всегда ограничено, и лишь несколько процессов мо-
могут работать одновременно. Компонент операционной системы, называемый
планировщиком, выбирает, какой процесс будет выполняться. В некоторых
операционных системах существуют только невытесняемые процессы, т. е.
планировщик активизируется, только когда какой-нибудь процесс добро-
добровольно освободит процессор. Однако в многопользовательской системе про-
процессы должны быть вытесняемыми. Операционная система следит за тем,
сколько времени каждый процесс занимает процессор, и периодически акти-
активизирует планировщик.
Unix является многозадачной операционной системой с вытесняемыми про-
процессами. Даже если ни один пользователь не работает в системе, и не выпол-
выполняется ни одно приложение, несколько системных процессов ведут монито-
мониторинг периферийных устройств. В частности, некоторые процессы прослуши-
прослушивают системные терминалы в ожидании входов пользователей в систему.
Когда кто-то вводит имя пользователя, прослушивающий процесс запускает
программу, проверяющую пароль. Если личность пользователя установлена,
этот процесс запускает другой, который выполняет оболочку, обрабатываю-
обрабатывающую введенные команды. Когда активизирован графический интерфейс,
каждое окно на дисплее обычно управляется отдельным процессом. Когда
пользователь создает графическую оболочку, один процесс работает с графи-
графическими окнами, а второй — выполняет оболочку, в которую пользователь
вводит команды. Для каждой команды пользователя процесс оболочки созда-
создает еще один процесс, который выполняет соответствующую программу.
Unix-подобные операционные системы придерживаются модели процесс/яд-
процесс/ядро. Каждый процесс испытывает иллюзию, что он единственный процесс на
компьютере, и что у него исключительный доступ к службам операционной
системы. Когда процесс делает системный вызов (то есть отправляет запрос
ядру, см. главу 10), оборудование переключает уровень привилегий с режима
пользователя на режим ядра, и процесс начинает выполнять процедуру ядра с
6 Некоторые многозадачные операционные системы не являются многопользовательскими; приме-
примером может служить Microsoft Windows 98.
четко определенной целью. Таким образом, операционная система действует
в рамках контекста выполняющегося процесса, чтобы удовлетворить его за-
запрос. Когда процесс будет полностью удовлетворен, процедура ядра застав-
заставляет аппаратную часть вернуться в режим пользователя, и процесс продолжа-
продолжает свое выполнение с инструкции, следующей за системным вызовом.
Архитектура ядра
Как было сказано ранее, большинство ядер Unix монолитны. Каждый слой
ядра интегрирован в программу ядра и работает в режиме ядра от имени те-
текущего процесса. В противоположность этому, микроядерные операционные
системы требуют от ядра реализации лишь очень небольшого набора функ-
функций, как правило, нескольких примитивов синхронизации, простого плани-
планировщика и механизма межпроцессного взаимодействия. Несколько систем-
системных процессов, работающих поверх микроядра, реализуют другие функции,
присущие операционной системе, такие как выделение памяти, драйверы
устройств и обработчики системных вызовов.
Хотя академические исследования в области операционных систем ориенти-
ориентированы на микроядра, такие системы обычно медленнее монолитных из-за
явного обмена сообщениями между различными слоями. Впрочем, операци-
операционные системы с микроядрами имеют некоторые теоретические преимущест-
преимущества над монолитными. Микроядра вынуждают системных программистов
принять модульный подход, потому что каждый слой операционной системы
является относительно независимой программой, которая должна взаимодей-
взаимодействовать с остальными слоями при помощи хорошо определенных и четких
программных интерфейсов. Кроме того, существующие сегодня микроядер-
микроядерные операционные системы могут быть довольно легко перенесены на другие
архитектуры, потому что все аппаратно-зависимые компоненты обычно ин-
инкапсулированы в код микроядра. Наконец, операционные системы с микро-
микроядром работают с оперативной памятью, как правило, эффективнее, чем мо-
монолитные, потому что системные процессы, не реализующие необходимую
функциональность, могут быть выгружены или уничтожены.
Чтобы добиться многих теоретических преимуществ микроядер без пониже-
понижения производительности, ядро Linux использует модули. Модуль — это объ-
объектный файл, код которого может быть скомпонован с ядром (или отсоеди-
отсоединен от него) на этапе выполнения. Объектный код обычно состоит из набора
функций, реализующих файловую систему, драйвер устройства или другие
функциональные возможности на верхнем слое ядра. В отличие от внешних
слоев микроядерных операционных систем, модуль не выполняется в виде
специального процесса. Он работает в режиме ядра от имени текущего про-
процесса, как любая другая статически скомпонованная функция ядра.
Основными достоинствами модулей являются следующие:
□ модульный подход — поскольку любой модуль может быть присоединен
и отсоединен на этапе выполнения, системным программистам приходит-
приходится применять хорошо определенные программные интерфейсы для обра-
обращения к структурам данных, обрабатываемых модулями. Такой подход
облегчает разработку новых модулей;
□ независимость от платформы — даже если модуль опирается на некото-
некоторые специфические аппаратные возможности, он не зависит от фиксиро-
фиксированной платформы. Например, модуль с драйвером диска, соответствую-
соответствующим стандарту SCSI, работает одинаково хорошо на IBM-совместимом
персональном компьютере и в системе Alpha от Hewlett-Packard;
□ экономное расходование оперативной памяти — модуль может быть
скомпонован с работающим ядром, когда потребуется его функциональ-
функциональность, и отсоединен, когда надобность в нем отпадает;
□ отсутствие снижения производительности — будучи подключенным, объ-
объектный код модуля эквивалентен объектному коду статически скомпоно-
скомпонованного ядра. Поэтому при вызове функций модуля не происходит явного
обмена сообщениями7.
Обзор файловой системы Unix
Строение операционной системы Unix сконцентрировано вокруг его файло-
файловой системы, которая обладает рядом интересных характеристик. Приведем
самые важные из них, поскольку они будут довольно часто упоминаться в
последующих главах.
Файлы
Файл в Unix— это хранилище информации, структурированное в виде по-
последовательности байтов, причем ядро не занимается интерпретацией содер-
содержимого файла. Многие программные библиотеки реализуют абстракции бо-
более высокого уровня, например записи, состоящие из полей, и адресацию за-
записей на основе ключей. Однако программы в этих библиотеках должны
использовать системные вызовы, предлагаемые ядром. С точки зрения поль-
пользователя, файлы организованы в древовидное пространство имен, пример ко-
которого изображен на рис. 1.1.
7 Незначительное снижение производительности все-таки происходит из-за подключения и отклю-
отключения модулей. Однако оно сравнимо со снижением, вызванным созданием и уничтожением сис-
системных процессов в микроядерных операционных системах.
Рис. 1.1. Пример дерева каталогов
Все узлы этого дерева, кроме листьев, обозначают имена каталогов. Узел ка-
каталога содержит информацию о файлах и каталогах, расположенных непо-
непосредственно под ним. Имя файла или каталога представляет собой последо-
последовательность произвольных символов ASCII8, за исключением слэша (/) и ну-
нулевого символа @). В большинстве файловых систем длина имени файла
ограничена; как правило, она не должна превышать 255 символов. Каталог,
соответствующий корню дерева, называется корневым каталогом. По согла-
соглашению, его имя олицетворяет слэш. В пределах каталога имена должны раз-
различаться, но одно и то же имя может присутствовать в разных каталогах.
С каждым процессом система ассоциирует текущий рабочий каталог (см.
разд. "Модель процесс/ядро" далее в этой главе). Он принадлежит контексту
выполнения процесса и идентифицирует каталог, используемый процессом в
данный момент. Для идентификации конкретного файла процесс использует
путь, который представляет собой последовательность чередующихся слэ-
слэшей и имен каталогов, ведущих к файлу. Если первым элементом пути явля-
является слэш, то про путь говорят, что он абсолютный, потому что его отправ-
отправной точкой является корневой каталог. Если же первым элементом является
имя каталога или файла, путь называется относительным, поскольку его от-
отправной точкой является текущий каталог процесса.
При указании имен файлов используются последовательности "." и "..". Они
обозначают текущий рабочий каталог и его родительский каталог соответст-
соответственно. Если текущий рабочий каталог является корневым, "." и ".." совпа-
совпадают.
8 В некоторых операционных системах разрешается выражать имена файлов символами нескольких
разных алфавитов; с этой целью используется 16-битовая расширенная кодировка символов, напри-
например, Unicode.
Жесткие и гибкие ссылки
Имя файла, содержащееся в каталоге, называется жесткой ссылкой или про-
просто ссылкой. У одного файла может быть несколько ссылок, включенных в
один и тот же или различные каталоги, иными словами, несколько имен.
Команда Unix
$ In pi p2
служит для создания новой жесткой ссылки, имеющей путь р2, на файл,
идентифицируемый путем pi.
На жесткие ссылки накладываются два ограничения:
□ Нельзя создать жесткую ссылку на каталог. Это могло бы превратить де-
дерево каталогов в циклический граф, что привело бы к невозможности най-
найти файл по его имени.
□ Ссылки могут быть созданы только среди файлов, входящих в одну фай-
файловую систему. Это серьезное ограничение, поскольку современные сис-
системы Unix могут включать в себя несколько файловых систем, располо-
расположенных на разных дисках или в разных разделах, причем пользователи
могут и не знать о физических границах между ними.
Для преодоления этих ограничений уже давно были введены гибкие ссылки
(также называемые символьными). Символьная ссылка— это небольшой
файл, содержащий произвольный путь к другому файлу. Этот путь может
вести к любому файлу или каталогу, расположенному в любой файловой сис-
системе. Он даже может ссылаться на несуществующий файл.
Команда Unix
$ In -s pi p2
создает новую символьную ссылку с путем р2, которая указывает на путь pi.
Когда выполняется эта команда, файловая система извлекает из р2 часть, со-
соответствующую каталогу, и создает в этом каталоге элемент типа "символь-
"символьная ссылка" с именем, обозначаемым путем р2. Этот новый файл содержит
имя, определяемое путем pi. Таким образом, любое обращение к р2 может
быть автоматически переадресовано к pi.
Типы файлов
Каждый файл Unix имеет один из следующих типов:
П обычный файл;
П1 каталог;
□ символьная ссылка;
□ файл блочного устройства;
□ файл символьного устройства;
□ канал и именованный канал (также называемый FIFO-файлом);
П1 сокет.
Файлы первых трех типов являются составными частями любой файловой
системы Unix. Их реализация подробно описана в главе 18.
Файлы устройств имеют отношение к устройствам ввода/вывода и к драйве-
драйверам устройств, интегрированных в ядро. Например, когда программа обраща-
обращается к файлу устройства, она действует непосредственно на устройство вво-
ввода/вывода, ассоциированное с этим файлом (см. главу 13).
Каналы и сокеты являются специальными файлами, используемыми при
взаимодействии процессов (см. разд. ''Синхронизация и критические облас-
области" далее в этой главе, см. также главу 19).
Дескриптор файла и индексный дескриптор
В Unix проводится четкое различие между содержимым файла и информаци-
информацией о файле. За исключением файлов устройств и файлов в специальных фай-
файловых системах, каждый файл является последовательностью байтов. Он не
включает в себя никакой управляющей информации, например, длины или
символа конца файла (EOF).
Вся информация, необходимая файловой системе для работы с файлом, со-
содержится в структуре данных, называемой индексным дескриптором. Каж-
Каждый файл имеет собственный индексный дескриптор, используемый файло-
файловой системой для идентификации файла.
Хотя файловые системы и функции ядра для работы с ними могут сильно
варьироваться от одной Unix-подобной системы к другой, они обязательно
должны предоставлять как минимум следующие атрибуты (сформулирован-
(сформулированные в стандарте POSIX):
□ тип файла (см. предыдущий раздел);
□ количество жестких ссылок, ассоциированных с файлом;
□ длина файла в байтах;
□ идентификатор устройства, содержащего файл;
□ номер индексного дескриптора, идентифицирующего файл в файловой
системе;
□ идентификатор пользователя-владельца файла;
□ идентификатор группы владельца;
□ несколько отметок времени, показывающих время изменения состояния
индексного дескриптора, время последнего обращения к файлу и время
последней модификации файла;
□ права доступа и режим файла.
Права доступа и режим файла
Потенциальные пользователи файла разбиваются на три класса:
□ пользователь, владеющий файлом;
□ пользователи, принадлежащие к той же группе, что и файл, кроме вла-
владельца;
□ все прочие пользователи.
Существует три типа прав доступа (чтение, запись и выполнение) для каждо-
каждого из перечисленных классов. Таким образом, набор прав доступа, ассоции-
ассоциированных с файлом, состоит из девяти разных двоичных флагов. Три допол-
дополнительных флага, называемых suid (Set User ID, установить идентификатор
пользователя), sgid (Set Group ID, установить идентификатор группы) и
sticky, определяют режим файла. Будучи применены к выполнимым файлам,
эти флаги несут следующий смысл:
□ suid— процесс, выполняющий файл, в нормальной ситуации сохраняет
идентификатор пользователя, которому он принадлежит. Однако если у
исполняемого файла установлен флаг suid, процесс получает идентифика-
идентификатор пользователя, владеющего файлом;
□ sgid— процесс, выполняющий файл, сохраняет идентификатор группы
пользователя, которому он принадлежит. Однако если у исполняемого
файла установлен флаг sgid, процесс получает идентификатор группы
файла;
П sticky— выполнимый файл с установленным флагом sticky соответству-
соответствует запросу к ядру с требованием оставить программу в памяти после окон-
окончания ее работы9.
Когда процесс создает файл, идентификатор владельца файла совпадает с
идентификатором пользователя процесса. Идентификатор группы владельца
файла— это либо идентификатор группы процесса, создавшего файл, либо
идентификатор группы родительского каталога, в зависимости от состояния
флага sgid родительского каталога.
9 Этот флаг морально устарел; сейчас применяются другие подходы, основанные на совместном
использовании страниц кода.
Системные вызовы для работы с файлами
Когда пользователь обращается к содержимому обычного файла или катало-
каталога, он фактически обращается к некоторым данным, хранящимся на аппарат-
аппаратном блочном устройстве. В этом смысле файловая система является пользо-
пользовательским представлением физической организации раздела жесткого диска.
Поскольку процесс в режиме пользователя не может непосредственно взаи-
взаимодействовать с низкоуровневыми аппаратными компонентами, каждая ре-
реальная файловая операция должна выполняться в режиме ядра. Поэтому
в операционной системе Unix определен ряд системных вызовов для работы
с файлами.
Во всех ядрах Unix большое внимание уделяется эффективной работе с блоч-
блочными аппаратными устройствами, как способу повысить общую производи-
производительность системы. В последующих главах мы затронем тему работы с фай-
файлами в Linux и, в частности, опишем, как ядро реагирует на соответствующие
системные вызовы. Для понимания этого материала читателю необходимо
знать, как используются основные вызовы для работы с файлами. Они описа-
описаны в следующих разделах.
Открытие файла
Процессы могут работать только с "открытыми" файлами. Чтобы открыть
файл, процесс делает системный вызов:
fd = open(path, flag, mode)
Три параметра системного вызова имеют следующий смысл:
□ path — путь (относительный или абсолютный) к открываемому файлу;
□ flag — способ открытия файла (для чтения, записи, чтения/записи, добав-
добавления). Параметр также может уточнить, следует ли создать файл, если он
не существует;
□ mode — права доступа к созданному новому файлу.
Системный вызов создает объект "открытый файл" и возвращает идентифи-
идентификатор, называемый дескриптором файла. Открытый файл содержит:
□ несколько структур, необходимых для работы с файлом, например, набор
флагов, показывающих, как был открыт файл, поле offset, определяющее
текущую позицию в файле, начиная с которой будет действовать следую-
следующая файловая операция (так называемый файловый указатель), и т. д.;
□ несколько указателей на функции ядра, которые могут быть вызваны про-
процессом. Набор допустимых функций зависит от значения параметра flag.
Объекты "открытые файлы" подробно обсуждаются в главе 12. Сейчас мы
ограничимся описанием некоторых общих свойств, определяемых семанти-
семантикой стандарта POSIX:
□ Дескриптор файла определяет взаимодействие между процессом и откры-
открытым файлом, а объект "открытый файл" содержит данные, необходимые
для этого взаимодействия. Один и тот же объект "открытый файл" может
быть идентифицирован несколькими дескрипторами файла в пределах од-
одного процесса.
□ Несколько процессов могут одновременно открыть один файл. В этом
случае система при каждом открытии выделяет файлу отдельный дескрип-
дескриптор и отдельный объект "открытый файл". Когда такое происходит, фай-
файловая система Unix не обеспечивает никакой синхронизации операций
ввода/вывода, выполняемых с этим файлом. В то же время существуют
несколько системных вызовов, например flock (), позволяющих процессам
синхронизировать свои действия по отношению ко всему файлу или его
фрагментам (см. главу 12).
Чтобы создать новый файл, процесс может сделать системный вызов creat (),
который обрабатывается ядром точно так же, как и open ().
Обращение к открытому файлу
Обычные файлы Unix допускают как последовательный, так и произвольный
доступ, а к файлам устройств и именованным каналам, как правило, осущест-
осуществляется последовательный доступ. При доступе любого из двух типов ядро
сохраняет файловый указатель в объекте "открытый файл", т. е. запоминает-
запоминается текущая позиция, с которой будет действовать на файл следующая опера-
операция чтения или записи.
Последовательный доступ подразумевается по умолчанию: системные вызо-
вызовы read о и write о всегда относятся к позиции, определяемой файловым
указателем. Чтобы изменить его значение, программа должна явным образом
сделать системный вызов i seek о. Когда файл открывается, ядро устанавли-
устанавливает файловый указатель в позицию первого байта файла (смещение 0).
Системный вызов lseeko имеет вид
newoffset = lseek(fd, offset, whence);
где параметры имеют такой смысл:
□ f d — дескриптор открытого файла;
Я offset — целое со знаком, которое служит для вычисления новой позиции
файлового указателя;
□ whence — флаг, который определяет, вычисляется ли новая позиция сло-
сложением значения offset с нулем (смещение от начала файла), с текущим
значением файлового указателя или с позицией последнего байта (смеще-
(смещение от конца файла).
Системный вызов read () имеет вид
nread = read(fd, buf, count);
где параметры имеют такой смысл:
□ f d — дескриптор открытого файла;
□ buf — адрес буфера в адресном пространстве процесса, в который будут
пересылаться данные;
□ count — количество байтов, подлежащих чтению.
При обработке системного вызова ядро пытается прочитать count байтов из
файла с дескриптором f d, начиная с текущего значения смещения в открытом
файле. В некоторых случаях (конец файла, пустой канал и др.) ядру не удает-
удается прочитать все count байтов. Возвращаемое значение nread показывает фак-
фактически прочитанное количество байтов. Файловый указатель обновляется
сложением его текущего значения со значением nread. Параметры системно-
системного вызова write () аналогичны только что описанным.
Закрытие файла
Если процессу больше не нужно обращаться к содержимому файла, он может
сделать системный вызов:
res = close(fd);
который освобождает объект "открытый файл", соответствующий дескрипто-
дескриптору файла fd. Когда процесс завершает работу, ядро закрывает все его файлы,
оставшиеся открытыми.
Переименование и удаление файла
Процессу не нужно открывать файл, чтобы переименовать или удалить его,
ведь такая операция действует не на содержимое файла, а на содержимое од-
одного или нескольких каталогов. Например, системный вызов
res = rename(oldpath, newpath);
изменяет имя ссылки на файл, а системный вызов
res = unlink(pathname);
уменьшает счетчик ссылок на файл и удаляет соответствующий элемент ка-
каталога. Файл удаляется, только когда счетчик ссылок достигнет нуля.
Обзор ядер Unix
Ядра Unix обеспечивают среду выполнения, в которой приложения могут ра-
работать. Таким образом, ядро должно реализовать некий набор служб и соот-
соответствующие интерфейсы. Приложения пользуются этими интерфейсами и
обычно не взаимодействуют с аппаратными устройствами напрямую.
Модель процесс/ядро
Как было сказано ранее, процессор может работать либо в режиме пользова-
пользователя, либо в режиме ядра. Некоторые процессоры имеют больше двух рабо-
рабочих состояний. Например, у микропроцессоров 80x86 их четыре. Однако все
стандартные ядра Unix ограничиваются режимом ядра и режимом пользова-
пользователя.
Когда программа выполняется в режиме пользователя, она не может напря-
напрямую обратиться к структурам данных ядра или к его программам. Однако
когда приложение выполняется в режиме ядра, эти ограничения снимаются.
У каждой модели процессора есть специальные инструкции для переключе-
переключения между режимом пользователя и режимом ядра. Обычно программа рабо-
работает в режиме пользователя, а в режим ядра переключается только при за-
запросе функций, предоставляемых ядром. Когда ядро удовлетворит запрос
программы, оно переводит программу обратно в режим пользователя.
Процессы являются динамическими сущностями, обычно имеющими ограни-
ограниченное время жизни в системе. Задача по созданию и уничтожению процес-
процессов, а также по их синхронизации делегируется группе процедур ядра.
Само ядро процессом не является. Оно выступает в роли менеджера процес-
процессов. Модель процесс/ядро предполагает, что процесс, которому требуются
услуги ядра, использует специальные программные конструкции, называе-
называемые системными вызовами. Любой системный вызов настраивает группу па-
параметров, идентифицирующую запрос процесса, а затем выполняет аппарат-
но-зависимую инструкцию процессора, чтобы переключить его из режима
пользователя в режим ядра.
Помимо пользовательских процессов в системах Unix имеется несколько
привилегированных процессов, называемых потоками ядра, которые обла-
обладают следующими характеристиками:
□ они работают в режиме ядра в адресном пространстве ядра;
□ они не взаимодействуют с пользователями, и, следовательно, им не нужны
терминальные устройства;
□ обычно они создаются на этапе запуска системы и остаются в ней вплоть
до ее отключения.
В однопроцессорной системе в каждый момент времени выполняется только
один процесс, и он может работать либо в режиме пользователя, либо в ре-
режиме ядра. Если он работает в режиме ядра, значит, процессор выполняет
какую-то процедуру ядра. На рис. 1.2 изображены примеры переходов между
режимом пользователя и режимом ядра. Процесс 1 в режиме пользователя
делает системный вызов, после чего переключается в режим ядра, и систем-
системный вызов обслуживается. Затем процесс 1 возобновляет работу в режиме
пользователя, пока не возникнет прерывание по таймеру, в результате чего
активизируется планировщик, работающий в режиме ядра. Происходит пере-
переключение процессов, и начинается выполнение процесса 2 в режиме пользо-
пользователя, которое продолжается, пока аппаратное устройство не сгенерирует
прерывание. Как следствие, процесс 2 переключается в режим ядра и обслу-
обслуживает прерывание.
Рис. 1.2. Переходы между режимом пользователя и режимом ядра
Ядра Unix делают нечто большее, чем простая обработка системных вызовов.
На практике процедуры ядра могут быть активированы разными способами:
□ процесс делает системный вызов;
□ процессор, выполняющий процесс, сигнализирует об исключении, т. е. о
ненормальной ситуации (например, о недопустимой инструкции). Ядро
обрабатывает исключение от имени процесса, приведшего к его возникно-
возникновению;
□ периферийное устройство посылает процессору сигнал прерывания, чтобы
уведомить его о некотором событии, таком как необходимость вмеша-
вмешательства, изменение состояния или завершение операции ввода/вывода.
Каждый сигнал прерывания обрабатывается программой ядра, называемой
обработчиком прерываний. Поскольку периферийные устройства работа-
ют асинхронно по отношению к процессору, прерывания возникают в не-
непредсказуемые моменты времени;
□ выполняется поток ядра. Поскольку он работает в режиме ядра, соответст-
соответствующая программа должна считаться частью ядра.
Реализация процесса
Чтобы ядро могло управлять процессами, каждый из них представлен деск-
дескриптором процесса, который содержит информацию о текущем состоянии
процесса.
Когда ядро останавливает выполнение процесса, оно сохраняет в дескрипторе
процесса текущее содержимое нескольких процессорных регистров, среди
которых следующие:
□ счетчик команд (PC) и указатель стека (SP);
□ регистры общего назначения;
□ регистры операций с плавающей точкой;
□ управляющие регистры процессора (слово состояния процессора), содер-
содержащие информацию о состоянии процессора;
□ регистры управления памятью, которые служат для отслеживания опера-
оперативной памяти, используемой процессом.
Когда ядро решает возобновить выполнение процесса, оно загружает в про-
процессорные регистры содержимое соответствующих полей дескриптора про-
процесса. Поскольку сохраненное значение счетчика команд указывает на инст-
инструкцию, следующую за последней выполненной инструкцией, процесс про-
продолжает работу с того места, на котором остановился.
Когда процесс не выполняется на процессоре, он ждет некоторое событие.
В ядрах Unix у процесса существует много состояний ожидания, которые
обычно реализуются очередями из дескрипторов процессов. Каждая (воз-
(возможно, пустая) очередь соответствует множеству процессов, ожидающих не-
некоторое конкретное событие.
Реентерабельные ядра
Все ядра Unix являются реентерабельными. Это означает, что несколько про-
процессов могут одновременно работать в режиме ядра. Конечно, в однопроцес-
однопроцессорной системе только один процесс будет выполняться, а другие будут бло-
блокированы в режиме ядра в ожидании процессора или окончания операции
ввода/вывода. Например, сгенерировав запрос на чтение с диска от имени
процесса, ядро передаст его на обработку контроллеру диска, а само возобно-
вит выполнение других процессов. Когда устройство удовлетворит запрос на
чтение, оно уведомит ядро прерыванием, и первый процесс сможет продол-
продолжить работу.
Одним из способов обеспечения реентерабельности является написание
функций таким образом, чтобы они модифицировали только локальные пе-
переменные и не затрагивали глобальные структуры. Такие функции называют-
называются реентерабельными. Однако реентерабельное ядро не ограничивается реен-
реентерабельными функциями (хотя некоторые ядра реального времени именно
так и реализованы). Оно вполне может включать в себя функции других ви-
видов и применять механизмы блокировки для гарантии того, что только один
процесс выполняет нереентерабельную функцию в данный момент времени.
Когда возникает аппаратное прерывание, реентерабельное ядро способно
приостановить выполнение текущего процесса, даже если он находится в ре-
режиме ядра. Эта способность очень важна, потому что она повышает пропуск-
пропускную способность контроллеров устройств, генерирующих прерывания. От-
Отправив сигнал прерывания, устройство ждет, чтобы процессор подтвердил
его прием. Если ядро реагирует быстро, контроллер устройства сможет вы-
выполнять другие задачи, пока процессор обрабатывает прерывание.
Посмотрим, как реентерабельность влияет на организацию самого ядра.
Управляющим трактом ядра называется последовательность инструкций,
выполняемых ядром для обработки системного вызова, исключения или пре-
прерывания.
В простейшем случае процессор выполняет управляющий тракт ядра после-
последовательно, с первой инструкции до последней. Если же случится одно из
следующих событий, процессор будет чередовать управляющие тракты ядра:
□ процесс, работающий в режиме пользователя, делает системный вызов, а
соответствующий управляющий тракт ядра убеждается, что запрос не мо-
может быть удовлетворен немедленно, и поэтому вызывает планировщик,
чтобы тот выбрал для выполнения другой процесс. В результате происхо-
происходит переключение процессов. Первый управляющий тракт ядра остается
незаконченным, а процессор возобновляет выполнение какого-нибудь
другого. В этом случае выполняются два управляющих тракта ядра, дей-
действующих от имени двух разных процессов;
□ процессор распознает исключение (например, попытку обратиться к стра-
странице, отсутствующей в памяти) во время выполнения управляющего трак-
тракта ядра. Первый управляющий тракт приостанавливается, а процессор на-
начинает выполнять необходимую процедуру обработки исключения. В на-
нашем примере такая процедура может выделить процессу новую страницу
и прочитать ее содержимое с диска. Когда процедура завершится, выпол-
выполнение первого управляющего тракта ядра может быть возобновлено.
В этом случае выполняются два управляющих тракта ядра, действующих
от имени одного процесса;
□ во время выполнения управляющего тракта ядра при включенных преры-
прерываниях возникает аппаратное прерывание. Первый управляющий тракт яд-
ядра остается незаконченным, а процессор начинает выполнение другого
управляющего тракта ядра для обработки прерывания. Когда обработчик
прерывания завершится, выполнение первого управляющего тракта ядра
будет возобновлено. В этом случае два управляющих тракта ядра работа-
работают в контексте выполнения одного процесса, и процессорное время отно-
относится на его счет. При этом обработчик прерывания не обязательно дейст-
действует от имени данного процесса;
□ аппаратное прерывание возникает, когда процессор работает при вклю-
включенном вытеснении в ядре и имеется более приоритетный процесс, дос-
доступный для выполнения. В этом случае первый управляющий тракт ядра
остается незаконченным, а процессор возобновляет выполнение другого
управляющего тракта ядра от имени процесса с более высоким приорите-
приоритетом. Это происходит, только если ядро было откомпилировано с поддерж-
поддержкой вытеснения в ядре.
На рис. 1.3 проиллюстрированы несколько примеров чередующихся и не че-
чередующихся управляющих трактов ядра. Рассмотрены три различных со-
состояния процессора:
□ выполнение процесса в режиме пользователя User;
П выполнение обработчика исключения или системного вызова Ехср;
П выполнение обработчика прерывания Intr.
Рис. 1.3. Чередование управляющих трактов ядра
Адресное пространство процесса
Каждый процесс выполняется в своем собственном адресном пространстве.
Процесс, работающий в режиме пользователя, имеет свой стек и собственные
области для данных и кода. Работая в режиме ядра, процесс обращается к об-
областям данных и кода ядра и пользуется другим своим стеком.
Поскольку ядро является реентерабельным, несколько управляющих трактов
ядра, относящихся к разным процессам, могут быть выполнены поочередно.
В этом случае каждый управляющий тракт ядра обращается к собственному
стеку ядра.
Хотя каждому процессу кажется, что он обращается к собственному закры-
закрытому адресному пространству, бывают периоды времени, когда часть адрес-
адресного пространства совместно используется несколькими процессами. В неко-
некоторых случаях такое совместное использование явно декларируется процес-
процессами, в других— является следствием автоматических действий ядра,
направленных на экономию памяти.
Если какая-нибудь программа, например, текстовый редактор, нужна для ра-
работы нескольким пользователям одновременно, она загружается в память
один раз, а ее код может быть совместно использован всеми, кто с ней рабо-
работает. Естественно, этого нельзя сказать о данных, поскольку у каждого поль-
пользователя данные свои. Такой вид совместного использования адресного про-
пространства обеспечивается ядром автоматически с целью экономии памяти.
Процессы могут совместно использовать части своего адресного пространст-
пространства в одном из вариантов реализации межпроцессного взаимодействия с при-
применением техники разделяемой памяти, впервые реализованной в System V и
поддерживаемой в Linux.
Наконец, Linux поддерживает системный вызов mmapo, который позволяет
отобразить часть файла или данные, хранящиеся на блочном устройстве, в
некую область в адресном пространстве процесса. Отображение в память
может оказаться хорошей альтернативой обычным операциям пересылки
данных (то есть чтения и записи). Если один файл совместно используется
несколькими процессами, его образ включен в адресное пространство каждо-
каждого из этих процессов.
Синхронизация и критические области
Реализация реентерабельных ядер требует наличия механизмов синхрониза-
синхронизации. Если управляющий тракт ядра приостанавливается во время работы с
какой-то структурой данных ядра, никакому другому управляющему тракту
ядра не разрешено работать с этой структурой, если только она не будет воз-
возвращена в непротиворечивое состояние. В противном случае действия двух
управляющих трактов ядра могут привести к порче данных.
Для примера, предположим, что глобальная переменная v содержит количе-
количество доступных элементов некоего системного ресурса. Первый управляю-
щий тракт ядра, назовем его А, читает значение переменной и определяет,
что свободен только один элемент ресурса. В этот момент другой тракт яд-
ядра, В, активизируется и считывает значение той же переменной, которое по-
прежнему равно 1. Тракт В уменьшает значение v и приступает к использова-
использованию элемента ресурса. Затем возобновляется выполнение тракта А. Посколь-
Поскольку он уже прочитал значение v, он предполагает, что может уменьшить его и
воспользоваться элементом ресурса, который уже занят трактом В. Дело кон-
кончается тем, что v содержит -1, а два управляющих тракта ядра пользуются
одним элементом ресурса с потенциально катастрофическими последствиями.
Если результат вычислений зависит от того, как распланировано выполнение
двух или более процессов, значит, код некорректен. Мы говорим, что имеет
место состояние гонки.
Вообще говоря, безопасное обращение к глобальной переменной может быть
обеспечено с помощью атомарных операций. В предыдущем примере порча
данных не произойдет, если оба управляющих тракта будут читать и умень-
уменьшать значение v в ходе одной непрерываемой операции. Однако ядра имеют
много таких структур данных, к которым нельзя обратиться за одну опера-
операцию. Например, невозможно за одну операцию удалить элемент из связного
списка, поскольку ядру приходится обращаться как минимум к двум указате-
указателям одновременно. Фрагмент кода, который должен быть до конца выполнен
любым процессом до того, как в него войдет другой процесс, называется
критической областью10.
Описанные проблемы могут возникнуть не только среди управляющих трак-
трактов ядра, но и среди процессов, совместно использующих общие данные.
Существует несколько способов синхронизации. В следующих разделах об-
обсуждается синхронизация управляющих трактов ядра.
Отключение вытеснения в ядре
Простейшее и радикальное решение проблем синхронизации в некоторых
традиционных ядрах Unix заключается в том, что эти ядра не допускают вы-
вытеснения. Когда процесс работает в режиме ядра, он не может быть произ-
произвольным образом приостановлен и замещен другим процессом. В результате
этого в однопроцессорной системе ядро может без опасений обращаться ко
всем структурам данных ядра, которые не обновляются обработчиками пре-
прерываний и исключений.
Конечно, процесс, работающий в режиме ядра, может добровольно освобо-
освободить процессор, но в этом случае он обязан оставить все структуры данных в
10 Проблемы синхронизации подробно освещаются в других книгах, к которым мы и отсылаем за-
заинтересованного читателя.
непротиворечивом состоянии. Более того, когда он возобновит свое выпол-
выполнение, он должен будет перепроверить значение каждой ранее прочитанной
структуры, которая могла быть изменена.
Механизм синхронизации в ядрах, допускающих вытеснение, состоит в от-
отключении вытеснения перед входом в критическую область и во включении
его сразу после выхода из нее.
В многопроцессорных системах отсутствия вытеснения в ядре недостаточно,
поскольку два управляющих тракта ядра, работающие на разных процессо-
процессорах, могут одновременно обратиться к одной структуре данных.
Отключение прерываний
Еще один механизм синхронизации, применяемый в однопроцессорных сис-
системах, состоит в отключении всех аппаратных прерываний перед входом в
критическую область с последующим их включением сразу после выхода из
нее. Этот механизм, хотя и прост, далеко не оптимален. Если критическая
область имеет большой размер, прерывания будут отключены на довольно
долгое время, что может привести в "замораживанию" активности всей аппа-
аппаратной части.
Кроме того, в многопроцессорной системе отключения прерываний на ло-
локальном процессоре недостаточно, и необходимо применять другие способы
синхронизации.
Семафоры
Широко используемый механизм, эффективный как в одно-, так и в много-
многопроцессорных системах, опирается на применение семафоров. Семафор —
это просто счетчик, ассоциированный со структурой данных. Все потоки ядра
проверяют его перед обращением к структуре. Семафор можно рассматри-
рассматривать как объект в составе:
□ целочисленной переменной;
П списка ждущих процессов;
П двух атомарных методов: down о и up ().
Метод down о уменьшает значение семафора. Если новое значение отрица-
отрицательно, метод заносит работающий процесс в список семафора и блокирует
его (то есть вызывает планировщик). Метод up о увеличивает значение сема-
семафора и, если новое значение больше или равно 0, активизирует один или не-
несколько процессов из списка.
Каждая структура данных, которая должна быть защищена, имеет собствен-
собственный семафор, который инициализируется единицей. Когда управляющему
тракту ядра нужно обратиться к структуре данных, он вызывает метод down ()
для соответствующего семафора. Если новое значение семафора неотрица-
неотрицательно, доступ к структуре разрешен. В противном случае процесс, выпол-
выполняющий этот тракт ядра, заносится в список семафора и блокируется. Когда
другой процесс выполнит метод up () для того же семафора, одному из про-
процессов, находящихся в списке, будет разрешено продолжать выполнение.
Спин-блокировки
В многопроцессорных системах семафоры не всегда являются наилучшим
решением проблем синхронизации. Некоторые структуры данных ядра долж-
должны быть защищены от попыток одновременного обращения к ним со стороны
управляющих трактов ядра, которые работают на разных процессорах. В этом
случае, если время обновления структуры данных невелико, семафор может
оказаться весьма неэффективным. Чтобы повторно проверить состояние се-
семафора, ядро должно занести процесс в список семафора, а затем приостано-
приостановить его. Поскольку обе операции являются относительно дорогими, за то
время, что требуется для их выполнения, другой управляющий тракт ядра
может уже освободить семафор.
В таких ситуациях многопроцессорные операционные системы применяют
спин-блокировки. Спин-блокировка во многом похожа на семафор, но у нее
нет списка процессов. Когда процесс обнаруживает, что другой процесс ус-
установил блокировку, он "крутится на месте", выполняя плотный цикл инст-
инструкций, пока блокировка не будет снята.
Конечно, в однопроцессорной среде от спин-блокировок нет никакой пользы.
Когда управляющий тракт ядра попытается обратиться к заблокированной
структуре данных, он запустит бесконечный цикл. В результате управляю-
управляющий тракт ядра, который обновляет защищенную структуру, потеряет всякую
возможность продолжить выполнение и освободить спин-блокировку. Ре-
Результатом будет зависание системы.
Предотвращение взаимных блокировок
Процессы или управляющие тракты ядра, синхронизированные с другими
управляющими трактами, могут легко впасть в состояние взаимной блоки-
блокировки. Простейший случай взаимной блокировки имеет место, когда про-
процесс р 1 получает доступ к структуре данных а, процесс р2 получает доступ к
структуре данных Ь, затем процесс pi ждет освобождения структуры b, a
процесс р2 — структуры а. Могут возникнуть и другие, более сложные цик-
циклические взаимные блокировки в целых группах процессов. Конечно же, вза-
взаимная блокировка полностью замораживает вовлеченные в нее процессы или
управляющие тракты ядра.
Когда речь идет о строении ядра, взаимные блокировки становятся предме-
предметом внимания при большом количестве блокировок в ядре. В этом случае
может оказаться весьма затруднительно гарантировать, что взаимные блоки-
блокировки не возникнут при всех возможных вариантах чередования управляю-
управляющих трактов ядра. В некоторых операционных системах, включая Linux, эта
проблема предотвращается путем установки определенного порядка получе-
получения блокировок.
Сигналы и взаимодействие между процессами
Сигналы Unix предоставляют механизм уведомления процессов о системных
событиях. Каждому событию сопоставлен индивидуальный номер сигнала,
обычно обозначаемый символьной константой, например, sigterm. Сущест-
Существует два вида системных событий:
□ асинхронные уведомления — когда пользователь отправляет сигнал пре-
прерывания sigint работающему процессу, нажимая на клавиатуре соответ-
соответствующую комбинацию (обычно <Ctrl>+<C>);
□ синхронные уведомления — ядро отправляет сигнал sigsegv процессу,
когда он пытается обратиться по недопустимому адресу памяти.
В стандарте POSIX определено около 20 различных сигналов, два из которых
определяются пользователем и могут служить в качестве примитивного ме-
механизма синхронизации процессов и взаимодействия между ними в режиме
пользователя. Вообще говоря, процесс может отреагировать на доставку сиг-
сигнала двумя способами:
□ игнорировать сигнал;
□ асинхронно выполнить определенную процедуру (обработчик сигнала).
Если процесс не использует ни один из этих вариантов, ядро выполняет дей-
действие по умолчанию, зависящее от номера сигнала. Возможны следующие
действия по умолчанию:
□ завершить выполнение процесса;
□ записать контекст выполнения и содержимое адресного пространства
в файл (выполнить core-дамп) и завершить выполнение процесса;
□ игнорировать сигнал;
□ приостановить процесс;
□ возобновить выполнение процесса, если он был приостановлен.
Обработка сигналов ядра является довольно сложным делом, потому что се-
семантика стандарта POSIX позволяет процессам временно блокировать сигна-
лы. Кроме того, сигналы sigkill и sigstop не могут быть непосредственно
обработаны процессом или проигнорированы.
Система Unix System V от AT&T представила другие виды взаимодействия
между процессами в режиме пользователя, которые были затем приняты во
многих ядрах Unix: семафоры, очереди сообщений и совместно используе-
используемую память. Они известны под коллективным именем System V IPC.
Ядро реализует эти конструкции как ресурсы взаимодействия между процес-
процессами. Процесс получает в свое распоряжение ресурс, делая системный вызов
shmget (), semget () ИЛИ rnsgget (). Подобно файлам, ресурсы взаимодействия
между процессами постоянны в том смысле, что они должны быть явно
уничтожены процессом-создателем, текущим владельцем или суперпользова-
суперпользовательским процессом.
Семафоры аналогичны описанным ранее в этой главе, с той разницей, что
они резервируются для процессов в режиме пользователя. Очереди сообще-
сообщений позволяют процессам обмениваться сообщениями с помощью системных
вызовов msgsnd () и msgrcv (), которые, соответственно, заносят сообщение в
специальную очередь и извлекают его оттуда.
Стандарт POSIX (IEEE Std 1003.1-2001) определяет механизм взаимодейст-
взаимодействия между процессами, основанный на очередях сообщений, которые так и
называются — очередями сообщений POSIX. Они аналогичны очередям со-
сообщений System V IPC, но имеют более простой, основанный на файлах ин-
интерфейс для приложений.
Совместно используемая память обеспечивает самый быстрый способ обмена
данными между процессами. Процесс начинает с того, что выдает системный
вызов shmget () и создает новую область совместно используемой памяти не-
необходимого размера. Получив идентификатор ресурса взаимодействия между
процессами, процесс делает системный вызов shmato, который возвращает
начальный адрес новой области в адресном пространстве процесса. Когда
процесс решает убрать совместно используемую память из своего адресного
пространства, он делает системный вызов shmdt (). Реализация совместно ис-
используемой памяти зависит от того, как ядро реализует адресное пространст-
пространство процесса.
Управление процессами
В системах Unix проводится строгое различие между процессом и програм-
программой, которую он выполняет. В соответствии с этим системные вызовы fork ()
и exit () применяются для создания нового процесса и для его завершения, а
системные вызовы семейства exec () служат для загрузки новой программы.
После выполнения такого вызова процесс возобновляет работу в совершенно
новом адресном пространстве, содержащем загруженную программу.
Процесс, сделавший системный вызов fork (), называется родителем, а новый
процесс — потомком. Родители и потомки легко находят друг друга, потому
что структура данных, описывающая каждый процесс, содержит указатель на
его непосредственного родителя и указатели на всех его непосредственных
потомков.
Наивная реализация системного вызова fork () потребовала бы копирования
данных и кода процесса-родителя и присваивания копий потомку. Это приве-
привело бы к большим затратам времени. Современные ядра, опирающиеся на ра-
работу аппаратных схем управления страницами, придерживаются подхода
"копирование при записи", при котором дублирование страниц откладывает-
откладывается до последнего момента (до того, когда родителю или потомку понадобится
записать данные в эту страницу). Реализация этого механизма в Linux описа-
описана в разд. "Копирование при записи1' главы 9.
Системный вызов exit о завершает выполнение процесса. Ядро обрабаты-
обрабатывает его, освобождая ресурсы, принадлежащие процессу, и отправляя его ро-
родителю сигнал sigchld, который, впрочем, по умолчанию игнорируется.
Процессы-зомби
Как процесс-родитель может узнать об окончании работы своих потомков?
Системный вызов wait4 () позволяет процессу ждать завершения работы од-
одного из его потомков; он возвращает идентификатор завершившегося процес-
процесса-потомка.
При выполнении этого системного вызова ядро проверяет, завершено ли вы-
выполнение потомка. Для представления завершившихся процессов существует
специальное состояние— зомби. Процесс остается в этом состоянии, пока
родитель не сделает системный вызов wait4 (). Обработчик этого системного
вызова получает данные об использовании ресурсов из полей дескриптора
процесса, и после сбора информации дескриптор может быть освобожден.
Если к моменту выполнения системного вызова wait4 () никакой потомок еще
не закончил работу, ядро обычно переводит процесс в состояние ожидания,
пока потомок не завершится.
Во многих ядрах дополнительно реализован системный вызов waitpid (), ко-
который позволяет процессу ждать окончания работы конкретного потомка.
Существует и иные варианты системного вызова wait4 ().
Считается хорошим стилем, когда ядро сохраняет информацию о процессе-
потомке, пока его родитель не сделает системный вызов wait4 о. Представим,
однако, что родитель завершится, так и не сделав этот системный вызов. Ин-
Информация будет занимать ценную память, которая могла бы быть использо-
использована "живыми" процессами. Реальный пример: многие оболочки позволяют
пользователю запустить команду в фоновом режиме и завершить сеанс. Про-
Процесс, выполняющий оболочку, завершится, а его потомки будут выполняться.
Проблема решается специальным системным процессом по имени init, кото-
который создается на этапе инициализации системы. Когда какой-либо процесс
завершается, ядро обновляет соответствующие указатели в дескрипторах
всех оставшихся потомков этого процесса так, что они становятся потомками
процесса init. Этот процесс следит за выполнением всех своих потомков и
периодически делает системный вызов wait4 о, побочным эффектом которо-
которого является удаление из системы всех осиротевших зомби.
Группы процессов и сеансы
В современных операционных системах Unix существует понятие группы
процессов, представляющее абстракцию "задание". Например, чтобы выпол-
выполнить команду
$ Is I sort I more
оболочка, поддерживающая группы процессов, такая как bash, создает новую
группу для трех процессов: is, sort и more. Таким образом, оболочка работает
с тремя процессами, как если бы они представляли одно целое (выражаясь
точнее, задание). Дескриптор каждого процесса имеет поле, содержащее
идентификатор группы процессов. Каждая группа процессов может иметь
лидера, т. е. процесс, у которого идентификатор совпадает с идентификато-
идентификатором группы. Только что созданный процесс заносится в группу его родителя.
Кроме того, в современных ядрах Unix существует понятие сеанса работы в
системе. С неформальной точки зрения, сеанс работы в системе включает в
себя все процессы, являющиеся потомками процесса, который начал сеанс на
конкретном терминале; обычно это первый процесс командной оболочки,
созданный для пользователя при его входе в систему. Все процессы в группе
должны находиться в одном сеансе. У сеанса может быть несколько групп
процессов, активных одновременно, причем всегда одна из этих групп нахо-
находится на переднем плане, т. е. имеет доступ к терминалу. Другие активные
группы процессов находятся в фоновом режиме. Когда фоновый процесс пы-
пытается обратиться к терминалу, он получает сигнал sigttin или sigttout. Во
многих командных оболочках для перевода группы процессов в фоновый ре-
режим и обратно используются встроенные команды bg и f g соответственно.
Управление памятью
Управление памятью до сих пор является самым сложным видом деятельно-
деятельности в ядре Unix. Более трети этой книги посвящено описанию того, как Linux
управляет памятью. В этом разделе освещены основные вопросы, относящие-
относящиеся к данной теме.
Виртуальная память
Во всех современных системах Unix поддерживается полезная абстракция,
называемая виртуальной памятью. Виртуальная память является логической
прослойкой между запросами приложения к памяти и аппаратным блоком
управления памятью или MMU (Memory Management Unit). Виртуальная па-
память используется для разных целей и имеет много достоинств:
□ несколько процессов могут выполняться параллельно;
□ можно выполнять приложения, у которых требования к доступной памяти
превышают физическую память, имеющуюся в наличии;
□ процессы могут выполнять программу, код которой загружен в память
лишь частично;
□ каждому процессу разрешено обращаться к подмножеству доступных фи-
физических адресов памяти;
□ процессы могут совместно использовать один образ библиотеки или про-
программы;
□ программы являются перемещаемыми, т. е. могут быть помещены в любое
место физической памяти;
□ программисты могут создавать машинно-независимый код, потому что им
не нужно учитывать физическую организацию памяти.
Главной составляющей частью подсистемы виртуальной памяти является по-
понятие виртуального адресного пространства. Множество ссылок на ячейки
памяти, которое может использовать процесс, отличается от множества фи-
физических адресов памяти. Когда процесс обращается по виртуальному адресу,
ядро и блок управления памятью сотрудничают в поисках действительного
физического адреса запрошенного элемента памяти.
Современные процессоры включают в себя электронные схемы, автоматиче-
автоматически преобразующие виртуальные адреса в физические. По этой причине вся
доступная оперативная память разбивается на страничные кадры (как прави-
правило, размером 4 или 8 Кбайт), а для установки соответствия между виртуаль-
виртуальными адресами и физическими вводится набор Таблиц Страниц. Такое аппа-
аппаратное решение упрощает выделение памяти, потому что запрос на блок
смежных виртуальных адресов может быть удовлетворен путем выделения
группы страничных кадров, имеющих несмежные физические адреса.
Использование оперативной памяти
Во всех операционных системах семейства Unix оперативная память четко
разбивается на две части. Несколько мегабайтов отводятся под хранение об-
раза ядра (то есть кода и статических данных ядра). Остальная оперативная
память обычно используется тремя различными способами:
□ для удовлетворения запросов ядра на буферы, дескрипторы и другие ди-
динамические структуры данных;
□ для удовлетворения запросов процессов на области памяти общего назна-
назначения и на отображение файлов в память;
□ для повышения производительности дисков и других устройств, исполь-
использующих буферизацию, с помощью кэшей.
Каждый тип запроса имеет свою ценность. С другой стороны, поскольку опе-
оперативная память всегда ограничена, необходим определенный баланс между
запросами, особенно когда свободной памяти осталось мало. Кроме того, ко-
когда достигается некоторый критический порог доступной памяти, и для осво-
освобождения памяти вызывается алгоритм утилизации страничных кадров, воз-
возникает вопрос, какие страничные кадры подлежат утилизации в первую оче-
очередь. Как мы увидим в главе 17, простого ответа на него нет, и какие-либо
теоретические обоснования практически отсутствуют. Единственным реше-
решением проблемы является разработка эмпирических алгоритмов, допускающих
тщательную настройку.
Одной из серьезных проблем, встающих перед системой виртуальной памяти,
является фрагментация памяти. В идеале, запрос на выделение памяти дол-
должен быть отвергнут, только если количество свободных страниц недостаточ-
недостаточно. Однако ядро часто вынуждено использовать физически смежные области
памяти. Поэтому бывает, что запрос на память не удовлетворяется, даже если
памяти достаточно, но она не представляет собой непрерывную область.
Аллокатор памяти ядра
Аллокатор памяти ядра — это подсистема ядра, которая пытается удовле-
удовлетворить запросы на области памяти, поступающие от всех компонентов ком-
компьютерной системы. Некоторые из этих запросов исходят от других подсис-
подсистем ядра, нуждающихся в памяти, а некоторые передаются через системные
вызовы пользовательскими программами, пытающимися увеличить адресные
пространства своих процессов. Хороший аллокатор памяти ядра должен об-
обладать следующими характеристиками:
□ работать быстро. На самом деле, это наиболее важное свойство аллокато-
ра, потому что он вызывается всеми подсистемами ядра (включая обра-
обработчики прерываний);
□ минимизировать объем непроизводительно расходуемой памяти;
□ по возможности, смягчить проблему фрагментации памяти;
□ уметь сотрудничать с остальными подсистемами управления памятью,
чтобы брать страничные кадры у них взаймы и отдавать обратно.
Существует целый ряд аллокаторов памяти ядра, использующих различные
алгоритмы работы, в том числе:
□ аллокатор карты ресурсов;
□ свободные списки "степеней двойки";
□ аллокатор МакКусика-Карелза;
□ buddy-система;
□ зонный аллокатор Mach;
□ аллокатор Dynix;
□ slab-аллокатор Solaris.
Как мы увидим в главе 8, аллокатор памяти ядра Linux — это slab-аллокатор,
построенный на базе buddy-системы.
Работа с пространством виртуальных адресов процесса
Адресное пространство процесса содержит все виртуальные адреса, к кото-
которым процессу разрешено обращаться. Ядро обычно хранит пространство вир-
виртуальных адресов процесса в виде списка дескрипторов областей памяти.
Например, когда процесс запускает выполнение какой-нибудь программы
с помощью системного вызова семейства exec (), ядро присваивает процессу
пространство линейных адресов, включающее в себя области памяти:
□ исполняемого кода программы;
□ проинициализированных данных программы;
□ неинициализированных данных программы;
□ начального стека программа (то есть стека режима пользователя);
□ исполняемого кода и данных библиотек, необходимых программе;
□ кучи (памяти, динамически запрошенной программой).
Во всех современных Unix-подобных операционных системах принята стра-
стратегия выделения памяти, называемая выделением страниц по требованию.
Такой подход позволяет процессу начать выполнение программы, когда в
физической памяти нет ни одной ее страницы. При попытке обратиться к от-
отсутствующей странице блок управления памятью генерирует исключение.
Обработчик исключения находит нужную область памяти, выделяет свобод-
свободную страницу и инициализирует ее соответствующими данными. Аналогич-
Аналогичным образом, когда программа динамически запрашивает память с помощью
системного вызова maiioco или brk() (последний делается также и в коде
maiioco), ядро лишь изменяет размер кучи, выделенной процессу. Странич-
Страничный кадр выделяется процессу только тогда, когда он провоцирует исключе-
исключение, пытаясь обратиться к своей виртуальной памяти.
Пространства виртуальных адресов делают возможными и другие эффектив-
эффективные стратегии, например копирование при записи, упомянутое ранее. Напри-
Например, когда создается новый процесс, ядро присваивает страничные кадры ро-
родителя адресному пространству потомка, помечая их как доступные только
для чтения. Если родитель или потомок попытается модифицировать содер-
содержимое страницы, будет возбуждено исключение. Обработчик этого исключе-
исключения присвоит процессу, совершившему попытку записи, новый страничный
кадр и проинициализирует его содержимым оригинальной страницы.
Кэширование
Значительная часть доступной физической памяти используется в качестве
кэша для жестких дисков и других блочных устройств. Дело в том, что дис-
дисководы работают очень медленно: время обращения к диску исчисляется
миллисекундами, а это очень много, по сравнению со временем обращения к
оперативной памяти. В результате, диски часто являются узким местом, с
точки зрения производительности системы. Вообще, одна из основных стра-
стратегий, реализованных уже в ранних версиях Unix, заключается в том, чтобы
отложить запись на диск до самого последнего момента. Получается, что
данные, ранее прочитанные с диска и больше не нужные ни одному процессу,
продолжают оставаться в оперативной памяти.
Эта стратегия основана на том факте, что новым процессам с большой веро-
вероятностью потребуются данные, прочитанные с диска или записанные на него
процессами, которые уже прекратили свою работу. Когда процесс запраши-
запрашивает доступ к диску, ядро вначале проверяет, находятся ли в кэше затребо-
затребованные данные. Каждый раз, когда проверка дает положительный результат
(это называется "попаданием в кэш"), ядро может обслужить запрос процес-
процесса, не обращаясь к диску.
Системный вызов sync () выполняет принудительную синхронизацию с дис-
диском, записывая на него все "грязные" буферы (то есть буферы, содержимое
которых отличается от содержимого соответствующих блоков диска). Чтобы
избежать потери данных, все операционные системы периодически сохраня-
сохраняют "грязные" буферы на диске.
Драйверы устройств
Ядро взаимодействует с устройствами ввода/вывода посредством драйверов.
Драйверы устройств включены в состав ядра и содержат структуры данных и
функции, управляющие одним или несколькими устройствами, такими как
жесткие диски, клавиатуры, мыши, мониторы, сетевые карты и устройства,
подключенные к шине SCSI. Каждый драйвер взаимодействует с остальными
частями ядра (в том числе с другими драйверами) через специальный интер-
интерфейс. Такой подход имеет следующие достоинства:
□ код, специфичный для устройства, может быть инкапсулирован в специ-
специальном модуле;
□ производители могут создавать новые устройства, не зная исходный код
ядра; им необходимо знать только спецификации интерфейса;
□ ядро обращается со всеми устройствами унифицированным образом и
взаимодействует с ними через единый интерфейс;
□ можно написать драйвер устройства как модуль, который будет динамиче-
динамически подключаться к ядру, не требуя перезагрузки системы. Кроме того,
имеется возможность динамически выгружать модуль, в котором больше
нет необходимости, тем самым минимизируя размер образа ядра в опера-
оперативной памяти.
На рис. 1.4 проиллюстрировано взаимодействие драйверов устройств с ос-
остальной частью ядра и с процессами.
Рис. 1.4. Интерфейс драйверов устройств
Некоторые пользовательские программы пытаются работать с аппаратными
устройствами. Они делают запросы к ядру с помощью обычных файловых
системных вызовов, а файлы устройств обычно находятся в каталоге /dev.
Фактически файлы устройств являются обращенной к пользователю частью
интерфейса драйверов устройств. Каждый файл устройства относится к кон-
конкретному драйверу, который вызывается ядром для выполнения запрошенной
операции над оборудованием.
Во времена, когда система Unix только появилась, графические терминалы
были дороги и не имели широкого распространения. Ядра Unix напрямую
работали только с алфавитно-цифровыми терминалами. Когда графические
терминалы стали обычной составляющей компьютерных систем, появились
специализированные приложения, например X Window System, которые за-
запускаются как стандартные процессы и напрямую обращаются к портам вво-
ввода/вывода графического интерфейса и к видеопамяти. В некоторых совре-
современных Unix-подобных операционных системах, например Linux 2.6, суще-
существует абстракция для фрейм-буфера графической карты, что позволяет
прикладным программам работать с графическими терминалами, ничего не
зная о портах ввода/вывода графического интерфейса (см. разд. "Уровни под-
поддержки ядра" гл. 13).
ГЛАВА 2
Адресация памяти
В этой главе обсуждается техника адресации. К счастью, операционная сис-
система не обязана самостоятельно управлять физической памятью. Современ-
Современные процессоры включают в себя ряд аппаратных компонентов, которые по-
повышают эффективность и надежность работы с памятью, так что программ-
программные ошибки не приводят к недопустимому обращению к памяти за пределами
программы.
Как и во всей книге, в этой главе мы подробно обсудим, как происходит ад-
адресация чипов памяти в микропроцессорах 80x86, а также как операционная
система Linux использует доступные ей аппаратные схемы адресации. Мы
надеемся, что, изучив детали реализации на самой популярной платформе, на
которой работает Linux, читатель лучше поймет общую теорию выделения
страниц и сможет разобраться с реализациями на других платформах.
Это первая из трех глав, посвященная управлению памятью. В главе 8 обсуж-
обсуждается, как ядро выделяет основную память для себя, а в главе 9 мы рассмот-
рассмотрим, как линейные адреса выделяются процессам.
Адреса памяти
Программисты обычно говорят об адресе, как о способе обратиться к содер-
содержимому ячейки памяти. Однако, имея дело с процессорами 80x86, мы долж-
должны различать три вида адресов:
О логический адрес— используется в инструкциях машинного языка для
обозначения адреса операнда или инструкции. Этот тип адресов относится
к широко известной сегментной архитектуре 80x86, которая заставляет
программистов, работающих в MS-DOS и Windows, разбивать программы
на сегменты. Каждый логический адрес состоит из сегмента и смещения,
которое определяет расстояние от начала сегмента до адресуемой ячейки;
□ линейный адрес (также известный как виртуальный адрес) — 32-разрядное
целое без знака, которое можно использовать для адресации до 4 Гбайт
D 294 967 296 ячеек памяти). Линейные адреса обычно представляются
в шестнадцатеричной нотации, и их значения лежат в диапазоне
от 0x00000000 до Oxffffffff;
□ физический адрес — используется для адресации ячеек в микросхемах па-
памяти. Представляется электрическими сигналами, посылаемыми с адрес-
адресных контактов микропроцессора на шину памяти. Физические адреса обо-
обозначаются 32- или 36-разрядными целыми без знака.
Блок управления памятью, или MMU (Memory Management Unit), преобразу-
преобразует логический адрес в линейный с помощью электронной схемы, которая на-
называется блоком сегментации. Затем другая электронная схема, называемая
блоком управления страницами, преобразует линейные адреса в физические
(рис. 2.1).
Рис. 2.1. Преобразование логического адреса
В многопроцессорных системах все процессоры, как правило, совместно ис-
используют одну память. Это означает, что процессоры могут одновременно и
независимо друг от друга обратиться к микросхемам оперативной памяти.
Поскольку операции чтения и записи на этих микросхемах должны выпол-
выполняться последовательно, устройство, называемое арбитром памяти, помеща-
помещается между шиной и каждой микросхемой оперативной памяти. Роль арбитра
в том, чтобы предоставлять доступ процессору, если микросхема свободна, и
откладывать запрос, если микросхема обслуживает другой процессор. Арбит-
Арбитры памяти существуют даже в однопроцессорных системах, потому что в та-
таких системах имеются специализированные процессоры, называемые кон-
контроллерами DMA (Direct Memory Access, прямой доступ к памяти), которые
работают параллельно с центральным процессором (см. главу 13). В много-
многопроцессорных системах структура арбитра сложнее, потому что у него боль-
больше входных портов. Например, в системах с двумя процессорами Pentium
поддерживается арбитр с двумя портами на входе каждой микросхемы памя-
памяти, причем требуется, чтобы эти два процессора обменивались синхронизи-
синхронизирующими сообщениями перед обращением к общей шине. От программиста
арбитр скрыт, поскольку управляется аппаратными схемами.
Сегментация в аппаратной части
Начиная с модели 80286, микропроцессоры Intel выполняют преобразование
адресов двумя разными способами, которые называются реальным режимом
и защищенным режимом. В следующих разделах мы сосредоточим внимание
на преобразовании адресов в защищенном режиме. Реальный режим сущест-
существует, в основном, для поддержки совместимости со старыми моделями и для
того, чтобы позволить операционной системе выполнить начальную загрузку
(см. приложение 1).
Селектор сегментов и сегментные регистры
Логический адрес состоит из двух частей: из идентификатора сегмента и
смещения, которое определяет относительный адрес внутри сегмента. Иден-
Идентификатор сегмента — это 16-битовое поле, называемое селектором сегмента
(рис. 2.2), а смещение — 32-битовое поле. Поля селектора сегмента мы
опишем в разд. "Быстрый доступ к дескрипторам сегментов" далее в этой
главе.
Рис. 2.2. Формат селектора сегмента
Чтобы упростить и ускорить обращение к селекторам сегментов, процессор
предоставляет разработчику сегментные регистры, единственным назначе-
назначением которых является хранение селекторов сегментов. Эти регистры извест-
известны как cs, ss, ds, es, f s и gs. Их только шесть, но программа может использо-
использовать один и тот же сегментный регистр для разных целей, сохраняя его со-
содержимое в памяти, а затем восстанавливая его.
Три сегментных регистра имеют специальное предназначение:
□ cs — регистр сегмента кода; он указывает на сегмент, содержащий инст-
инструкции программы;
□ ss — регистр сегмента стека; он указывает на сегмент, содержащий стек
выполняющейся в данный момент программы;
□ ds — регистр сегмента данных; он указывает на сегмент, содержащий гло-
глобальные и статические данные.
Остальные сегментные регистры являются регистрами общего назначения и
могут ссылаться на любые сегменты данных.
У регистра cs есть еще одна важная функция: он включает в себя поле из
двух битов, определяющее текущий уровень привилегий процессора (Current
Privilege Level, CPL). Ноль обозначает наивысший уровень привилегий, а 3 —
самый низкий. В Linux задействованы только уровни 0 и 3, которые называ-
называются режимом ядра и режимом пользователя соответственно.
Дескрипторы сегментов
Каждый сегмент представлен 8-байтовым дескриптором сегмента, который
описывает его характеристики. Дескрипторы сегментов хранятся либо в гло-
глобальной таблице дескрипторов, GDT (Global Descriptor Table), либо в локаль-
локальной таблице дескрипторов, LDT (Local Descriptor Table).
Обычно определена только одна глобальная таблица дескрипторов, а каждо-
каждому процессу разрешается иметь собственную локальную таблицу дескрипто-
дескрипторов, если ему нужно создать новые сегменты в дополнение к тем, которые
хранятся в таблице GDT. Адрес и размер глобальной таблицы дескрипторов в
основной памяти содержится в управляющем регистре gdtr, а адрес и размер
текущей локальной таблицы дескрипторов — в управляющем регистре ldtr.
На рис. 2.3 изображен формат дескриптора сегмента, а значение его полей
разъясняется в табл. 2.1.
Таблица 2.1. Поля дескриптора сегмента
Имя поля Описание
Base Содержит линейный адрес первого байта сегмента
G Флаг гранулярности. Когда он сброшен (равен нулю), размер сегмента
выражен в байтах; в противном случае единица измерения равна
4096 байтам
Limit Содержит смещение последней ячейки памяти в сегменте, тем самым
ограничивая его длину. Когда флаг G сброшен, размер сегмента может
лежать в диапазоне от 1 байта до 1 Мбайт; в противном случае —
от 4 Кбайт до 4 Гбайт
s Системный флаг. Когда он сброшен, сегмент является системным и хра-
хранит критические структуры данных, такие как локальная таблица деск-
дескрипторов; в противном случае это обычный сегмент кода или данных
Туре Характеризует тип сегмента и его права доступа
dpl Уровень привилегий дескриптора. Используется для ограничения досту-
доступа к сегменту. Определяет минимальный уровень привилегий процессо-
процессора, необходимый для обращения к сегменту. То есть сегмент, у которого
поле dpl содержит 0, доступен только если текущий уровень привилегий
равен 0 (в режиме ядра), а сегмент с полем dpl, равным 3, доступен при
любом значении текущего уровня привилегий
Таблица 2.1 (окончание)
Имя поля Описание
р Флаг присутствия сегмента. Он сброшен, если сегмент в данный момент
отсутствует в основной памяти. Linux всегда устанавливает этот флаг
(бит 47), потому что не выгружает целые сегменты на диск
D или в Называется D или в в зависимости от того, содержит ли сегмент код или
данные. Назначение поля немного разное в каждом из двух случаев, но,
в принципе, бит установлен (равен 1), если адреса, используемые в ка-
качестве смещений в сегменте, имеют длину 32 бита, и сброшен (равен 0),
если их длина 16 бит (подробности см. в документации Intel)
avl Может быть использован в операционной системе, но Linux игнорирует
это поле
Рис. 2.3. Формат дескриптора сегмента
Существует несколько типов сегментов и, следовательно, их дескрипторов.
Перечислим те, что активно используются в Linux:
□ дескриптор сегмента кода — означает, что дескриптор ссылается на сег-
сегмент кода. Он может содержаться либо в глобальной, либо в локальной
таблице дескрипторов. Флаг s у этого дескриптора установлен (сегмент не
является системным);
□ дескриптор сегмента данных— означает, что дескриптор ссылается на
сегмент данных. Он может содержаться либо в глобальной, либо в локаль-
локальной таблице дескрипторов. Флаг s у этого дескриптора установлен. Сег-
Сегменты стека реализуются с помощью обычных сегментов данных;
□ дескриптор сегмента состояния задачи — означает, что дескриптор ссыла-
ссылается на сегмент состояния задачи— служит для хранения содержимого
процессорных регистров (см. разд. "Сегмент состояния задачи1' главы 3).
Этот дескриптор может находиться только в глобальной таблице дескрип-
дескрипторов. Поле туре имеет значение 11 или 9, в зависимости от того, выпол-
выполняется ли соответствующий процесс в данный момент. Флаг s у таких де-
дескрипторов сброшен;
□ дескриптор локальной таблицы дескрипторов — означает, что дескриптор
ссылается на сегмент, содержащий таблицу LDT. Этот дескриптор может
находиться только в глобальной таблице дескрипторов. Поле туре имеет
значение 2. Флаг s у таких дескрипторов сброшен. В следующем разделе
мы покажем, как процессоры 80x86 определяют, хранится ли дескриптор в
глобальной таблице или в локальной таблице процесса.
Быстрый доступ к дескрипторам сегментов
Вспомним, что логический адрес состоит из 16-битового селектора сегмента
и 32-битового смещения, а сегментные регистры содержат только селектор
сегментов.
Чтобы ускорить преобразование логических адресов в линейные, процессор
80x86 использует дополнительный непрограммируемый регистр (то есть та-
такой, значение которого не может быть задано программистом) с каждым из
шести программируемых регистров сегментации. Каждый непрограммируе-
непрограммируемый регистр содержит 8-байтовый дескриптор сегмента (описанный в преды-
предыдущем разделе), определяемый селектором сегмента, хранящимся в соответ-
соответствующем регистре сегменте. Всякий раз, когда в сегментный регистр загру-
загружается селектор сегмента, нужный дескриптор сегмента загружается из
памяти в соответствующий непрограммируемый регистр процессора. С этого
момента преобразование логических адресов, относящихся к данному сег-
сегменту, может быть выполнено без обращения к глобальной или локальной
таблице дескрипторов, хранящейся в основной памяти. Процессор может на-
напрямую ссылаться на регистр, содержащий дескриптор сегмента.
Обращение к глобальной или локальной таблице дескрипторов будет необхо-
необходимо только при изменении содержимого сегментных регистров (рис. 2.4).
Любой селектор сегмента содержит три поля, описанные в табл. 2.2.
Рис. 2.4. Селектор сегмента и дескриптор сегмента
Таблица 2.2. Поля селектора сегмента
Имя поля Описание
index Идентифицирует запись в таблице GDT или LDT, содержащую
дескриптор сегмента
TI Индикатор таблицы. Определяет, входит ли дескриптор сегмента в
глобальную (TI = 0) или локальную (тт. = 1) таблицу дескрипторов
RPL Уровень привилегий запрашивающего процессора. Задает текущий
уровень привилегий процессора, когда соответствующий селектор
сегмента загружен в регистр cs. Может быть также использован для
избирательного ослабления привилегий процессора при обращении
к сегментам данных
Поскольку дескриптор сегмента имеет длину 8 байтов, его относительный
адрес в глобальной или локальной таблице дескрипторов получается умно-
умножением 13-битового индексного поля селектора сегмента на 8. Например,
если таблица GDT находится по адресу 0x00020000 (значение хранится
в регистре gdtr), а индекс, заданный селектором сегмента, равен 2, адрес со-
соответствующего дескриптора сегмента равен 0x00020000 + B х8), или
0x00020010.
Первая запись глобальной таблицы дескрипторов всегда содержит 0. В ре-
результате, логические адреса с нулевым селектором сегмента будут считаться
ошибочными, и процессор будет возбуждать исключение. Максимальное ко-
количество дескрипторов сегментов в глобальной таблице дескрипторов рав-
равно 8191 (тоесть213-1).
Блок сегментации
На рис. 2.5 подобно проиллюстрировано, как логический адрес преобразуется
в линейный.
Рис. 2.5. Преобразование логического адреса
Блок сегментации выполняет следующие операции:
□ проверяет поле ti селектора сегмента, чтобы определить, которая таблица
дескрипторов содержит дескриптор сегмента. Это поле показывает, что
дескриптор хранится либо в глобальной таблице дескрипторов (и в этом
случае блок сегментации получает базовый линейный адрес таблицы из
регистра gdtr), либо в активной локальной (и тогда блок сегментации по-
получает базовый линейный адрес таблицы из регистра ldtr);
□ вычисляет адрес дескриптора сегмента по полю index селектора сегмента.
Поле index умножается на 8 (размер дескриптора сегмента), а результат
складывается с содержимым регистра gdtr или ldtr;
□ складывает смещение логического адреса с полем Base дескриптора сег-
сегмента и получает линейный адрес.
Обратите внимание, что благодаря существованию непрограммируемых ре-
регистров, ассоциированных с регистрами сегментации, первые две операции
необходимы, лишь если содержимое регистра сегментации менялось.
Сегментация в Linux
Сегментация появилась в микропроцессорах 80x86, чтобы стимулировать
программистов на разбиение приложений на логически связанные состав-
составляющие, такие как подпрограммы и области глобальных или локальных дан-
данных. Однако операционная система Linux пользуется сегментацией очень ог-
ограниченно. На практике сегментация и выделение страниц в определенной
степени функционально перекрываются, поскольку оба подхода можно при-
применять для разбиения физического адресного пространства процессов. Сег-
Сегментация присваивает процессам разные адресные пространства, а выделение
страниц отображает одно пространство линейных адресов в разные простран-
пространства физических адресов. Linux предпочитает выделение страниц сегмента-
сегментации по следующим причинам:
□ управление памятью упрощается, когда все процессы работают с одинако-
одинаковыми значениями сегментных регистров, т. е. когда они совместно исполь-
используют одно множество линейных адресов;
□ одной из целей создания Linux была переносимость на широкий спектр
архитектур. Архитектуры RISC, например, имеют ограниченную под-
поддержку сегментации.
В версии Linux 2.6 сегментация используется только в тех случаях, когда это-
этого требует архитектура 80x86.
В Linux все процессы, работающие в режиме пользователя, обращаются к
одной паре сегментов при адресации инструкций и данных. Эти сегменты
называются сегментом пользовательского кода и сегментом пользователь-
пользовательских данных соответственно. Аналогично, все процессы, работающие в ре-
режиме ядра, для этих целей также обращаются к одной паре сегментов —
к сегменту кода ядра и к сегменту данных ядра. В табл. 2.3 приводятся значе-
значения полей дескриптора сегмента для этих четырех важных сегментов.
Таблица 2.3. Значения полей дескрипторов четырех основных сегментов в Linux
Сегмент Base G Limit S Type DPL D/B P
Пользовательский 0x00000000 1 Oxfffff 1 10 3 1 1
код
Пользовательские 0x00000000 1 Oxfffff 12 3 11
данные
Код ядра 0x00000000 1 Oxfffff 1 10 0 11
Данные ядра 0x00000000 1 Oxfffff 12 0 11
Соответствующие селекторы сегментов определяются макросами usercs,
user_ds, kernel_cs и kernel_ds. Например, чтобы адресовать сегмент
кода ядра, ядро просто загружает значение, возвращенное макросом
kernelcs, в сегментный регистр cs.
Обратите внимание, что все линейные адреса, ассоциированные с этими сег-
сегментами, начинаются с нулевого и достигают предела, равного 232-1. Это оз-
означает, что все процессы, будь то в пользовательском режиме или режиме
ядра, могут использовать одни и те же линейные адреса.
Другим важным следствием того факта, что все сегменты начинаются с
0x00000000, является совпадение логических адресов с линейными в Linux.
Иначе говоря, значение смещения логического адреса всегда равно значению
соответствующего линейного адреса.
Как было сказано ранее, текущий уровень привилегий процессора показыва-
показывает, работает ли процессор в режиме пользователя или режиме ядра, и задается
полем rpl селектора сегмента, хранящегося в регистре cs. Когда текущий
уровень привилегий меняется, некоторые сегментные регистры должны быть
обновлены. Например, если текущий уровень привилегий равен 3 (режим
пользователя), регистр ds должен содержать селектор сегмента пользователь-
пользовательских данных, а когда этот уровень равен 0, регистр ds должен содержать се-
селектор сегмента данных ядра.
Аналогичная ситуация складывается в отношении регистра ss. Он должен
указывать на стек пользовательского режима в сегменте пользовательских
данных, когда текущий уровень привилегий равен 3, и на стек ядра в сегмен-
сегменте данных ядра, когда этот уровень равен 0. При переключении из пользова-
пользовательского режима в режим ядра Linux следит за тем, чтобы регистр ss содер-
содержал селектор сегмента данных ядра.
При сохранении указателя на инструкцию или структуру данных ядру не
нужно сохранять компонент логического адреса, относящийся к селектору
сегмента, потому что регистр ss содержит текущий селектор сегмента. На-
Например, когда ядро вызывает функцию, оно выполняет ассемблерную инст-
инструкцию call, передавая только компонент логического адреса, определяю-
определяющий смещение, и при этом подразумевается, что на селектор сегмента указы-
указывает регистр cs. Поскольку есть только один сегмент, выполняемый в режиме
ядра, а именно сегмент кода, идентифицируемый макросом kernelcs, бу-
будет достаточно загрузить значение kernelcs в регистр cs, как только про-
процессор переключится в режим ядра. То же самое справедливо для указателей
на структуры данных ядра (неявно использующих регистр ds) и указателей на
пользовательские структуры (регистр es используется ядром явно).
В дополнение к четырем только что описанным сегментам система Linux ис-
использует еще несколько специализированных сегментов. Мы представим их
читателю в следующем разделе, когда будем описывать глобальную таблицу
дескрипторов ядра.
Глобальная таблица дескрипторов Linux
В однопроцессорных системах существует только одна глобальная таблица
дескрипторов, в многопроцессорных — у каждого процессора есть своя таб-
таблица GDT. Все эти таблицы хранятся в массиве cpugdttabie, а их адреса и
размеры (используемые при инициализации регистров gdtr)— в массиве
cpugdtdescr. Эти символы определены в файле arch/i386/kernel/head.S.
Структура глобальных таблиц дескрипторов изображена на рис. 2.6. Каждая
таблица GDT включает в себя 18 дескрипторов сегментов и 14 нулевых, не-
неиспользуемых или зарезервированных элементов. Неиспользуемые элементы
введены в таблицу, чтобы дескрипторы сегментов, которые обычно запраши-
запрашиваются вместе, находились в одной 32-байтовой строке аппаратного кэша
(см. разд. "Аппаратный кэш" далее в этой главе).
Рис. 2.6. Глобальная таблица дескрипторов
Восемнадцать дескрипторов, хранящихся в каждой таблице GDT, указывают
на следующие сегменты:
□ четыре сегмента кода и данных пользователя и ядра;
□ сегмент состояния задачи, свой у каждого процессора в системе. Про-
Пространство линейных адресов, соответствующее этому сегменту, является
небольшим подмножеством пространства линейных адресов, соответст-
соответствующего сегменту данных ядра. Сегменты состояния задачи хранятся в
массиве inittss последовательно. В частности, поле Base дескриптора
сегмента состояния задачи для п-ro процесса указывает на и-й элемент
массива inittss. Флаг g сброшен, а поле Limit содержит значение Oxeb,
потому что длина этого сегмента равна 236 байтов. Поле туре содержит 9
или 11 (доступен 32-битовый сегмент состояния задачи), а поле dpl рав-
равно 0, потому что процессам, работающим в режиме пользователя, не раз-
разрешается обращаться к сегментам состояния задачи. Подробности о рабо-
работе Linux с этими сегментами можно найти в разд. "Сегмент состояния за-
задачи" главы 3;
П сегмент, включающий локальную таблицу дескрипторов, принятую по
умолчанию, которой, как правило, совместно пользуются все процессы;
□ сегменты TLS (Thread-Local Storage, локальная память потока). Это меха-
механизм, позволяющий многопоточным приложениям использовать до трех
сегментов, содержащих данные, независимые для каждого потока. Сис-
Системные ВЫЗОВЫ setthreadarea () И getthreadarea (), соответственно,
создают и освобождают сегмент TLS выполняемого процесса;
□ три сегмента, имеющих отношение к усовершенствованному управлению
питанием, или АРМ (Advanced Power Management). Код BIOS в своей ра-
работе пользуется сегментами, так что, когда АРМ-драйвер Linux вызывает
функции BIOS, чтобы прочитать или установить статус АРМ-устройств,
ему могут потребоваться специальные сегменты кода и данных;
□ пять сегментов, имеющих отношение к службам PnP (Plug and Play) BIOS.
Как и в предыдущем случае, код BIOS в своей работе использует сегмен-
сегменты, так что, когда PnP-драйвер Linux вызывает функции BIOS, чтобы оп-
определить ресурсы, используемые PnP-устройствами, ему могут потребо-
потребоваться специальные сегменты кода и данных;
□ специальный сегмент состояния задачи, используемый кодом ядра для об-
обработки исключений двойных ошибок (см. разд. "Исключения"главы 4).
Как было сказано ранее, у каждого процессора в системе имеется своя копия
глобальной таблицы дескрипторов. Все копии таблицы содержат идентичные
записи, за исключением нескольких случаев. Во-первых, у каждого процес-
процессора есть собственный сегмент состояния задачи, так что соответствующие
записи в таблицах GDT различаются. Кроме того, некоторые записи в гло-
глобальной таблице дескрипторов зависят от процесса, выполняемого в данный
момент (дескрипторы таблицы LDT и сегментов TLS). Наконец, в отдельных
ситуациях процессор может временно изменить запись в своей копии гло-
глобальной таблицы дескрипторов; например, это происходит при вызове про-
процедуры BIOS для системы АРМ.
Локальные таблицы дескрипторов Linux
Большинство приложений пользовательского режима Linux не обращается к
локальной таблице дескрипторов, и поэтому ядро определяет таблицу LDT по
умолчанию, чтобы процессы могли ее использовать совместно. Эта таблица
хранится в массиве defauitidt. Она содержит пять записей, но только две
из них фактически используются ядром: шлюз вызова для исполняемых
файлов iBCS, и шлюз вызова для исполняемых файлов Solaris/х86 (см.
разд. "Области выполнения" в главы 20). Шлюзы вызовов— это механизм,
предоставляемый микропроцессорами 80x86 для изменения уровня привиле-
привилегий процессора при вызове предварительно определенной функции. По-
Поскольку мы не будем обсуждать их в этой книге, читателю, интересующемуся
подробностями, следует обратиться к документации Intel.
Впрочем, в некоторых случаях процессу требуется собственная локальная
таблица дескрипторов. Оказывается, что это полезно для приложений (таких
как Wine), которые выполняют программы для Microsoft Windows, ориенти-
ориентированные на сегментацию. Системный вызов modifyidt о позволяет процес-
процессу установить собственную таблицу LDT.
Под любую "личную" таблицу LDT, созданную системным вызовом
modifyidt о, требуется отдельный сегмент. Когда процессор начинает вы-
выполнять процесс, имеющий собственную локальную таблицу дескрипторов,
запись об этой таблице в копии глобальной таблицы дескрипторов, принад-
принадлежащей этому процессу, изменяется соответствующим образом.
Приложения пользовательского режима также могут выделять новые сегмен-
сегменты с помощью системного вызова modif y_idt (), но ядро никогда не пользует-
пользуется этими сегментами и не обязано отслеживать соответствующие дескрипто-
дескрипторы сегментов, потому что они включены в локальную таблицу дескрипторов
процесса.
Управление страницами
на аппаратном уровне
Блок управления страницами преобразует линейные адреса в физические.
Одной из главных задач этого устройства является сопоставление типа запро-
запроса к памяти с правами доступа, связанными с линейным адресом. В случае
недопустимого обращения к памяти, блок управления страницами генерирует
исключение "ошибка обращения к странице" (см. главы 4 и 8).
Из соображений эффективности линейные адреса группируются в интервалы
фиксированной длины, называемые страницами. Смежные линейные адреса
в пределах одной страницы отображаются в смежные физические адреса. Та-
Таким образом, ядро может задать физический адрес страницы и права доступа
к ней вместо того, чтобы задавать физические адреса и права доступа ко всем
линейным адресам, входящим в эту страницу. Следуя общепринятой практи-
практике, мы будем термином "страница" обозначать как множество линейных ад-
адресов, так и данные, содержащиеся по этим адресам.
Блок управления страницами считает, что вся оперативная память разбита на
страничные кадры фиксированной длины (иногда называемые физическими
страницами). Каждый страничный кадр содержит одну страницу, т. е. его
длина равна длине страницы. Страничный кадр является составной частью
оперативной памяти и, следовательно, областью для хранения данных. Необ-
Необходимо проводить четкое различие между страницей и страничным кадром.
Страница — это всего лишь блок данных, который может храниться в любом
страничном кадре или на диске.
Структуры данных, отображающие линейные адреса в физические, называ-
называются таблицами страниц. Они хранятся в основной памяти и должным обра-
образом проинициализированы ядром до включения блока управления страни-
страницами.
Начиная с модели 80386, все процессоры 80x86 поддерживают разбивку на
страницы. Она включается путем установки флага pg в управляющем регист-
регистре его. Если pg = о, линейные адреса интерпретируются как физические.
Обычное управление страницами
Начиная с процессоров 80386, блок управления страницами в процессорах
Intel обрабатывает 4 Кбайт страниц.
Тридцать два бита линейного адреса разделяются на три поля:
□ каталог Directory — старшие 10 битов;
□ таблица Table — средние 10 битов;
□ смещение offset — младшие 12 битов.
Преобразование линейных адресов происходит посредством двух таблиц.
Первая таблица преобразования называется каталогом страниц, а вторая —
Таблицей Страниц1.
Целью создания такой двухуровневой схемы является сокращение объема
памяти, необходимого для Таблиц Страниц процессов. При наличии простой
одноуровневой Таблицы Страниц потребовалось бы до 22 записей (или, если
запись занимает 4 байта, 4 Мбайт оперативной памяти), чтобы представить
Таблицу Страниц для каждого процесса (при условии, что процесс использу-
использует все 4 Гбайт пространства линейных адресов), даже если процесс не обра-
обращается к каждому адресу в этом диапазоне. Двухуровневая схема сокращает
объем используемой памяти, поскольку требует Таблицы Страниц только для
тех виртуальных областей, к которым фактически обращается процесс.
1 Далее в этой книге термин "таблица страниц" (написанный строчными буквами) означает любую
страницу, хранящую отображение линейных адресов в физические. В то же время термин "Таблица
Страниц" (с заглавных букв) означает страницу на последнем уровне иерархии страничных таблиц.
Каждый активный процесс должен иметь каталог страниц. В то же время от-
отсутствует необходимость выделять память сразу под все Таблицы Страниц
процесса. Будет гораздо эффективнее выделять оперативную память для Таб-
Таблицы Страниц, только когда процесс действительно нуждается в ней.
Физический адрес используемого каталога страниц хранится в управляющем
регистре сгз. Поле Directory линейного адреса определяет запись в каталоге
страниц, указывающую на соответствующую Таблицу Страниц. Поле Table
адреса, со своей стороны, определяет запись в Таблице Страниц, содержа-
содержащую физический адрес страничного кадра, включающего в себя страницу.
Поле offset определяет относительную позицию внутри страничного кад-
кадра (рис. 2.7). Поскольку оно занимает 12 бит, каждая страница содержит
4096 байтов данных.
Рис. 2.7. Страничная организация в архитектуре 80x86
Поля Directory и Table имеют длину 10 битов, поэтому каталоги страниц и
Таблицы Страниц могут содержать до 1024 записей. Отсюда следует, что ка-
каталог страниц может адресовать до 1024 х 1024 х 4096 = 232 ячеек памяти, как
и следовало ожидать при 32-разрядной адресации.
Записи в каталогах страниц и в Таблицах Страниц имеют одинаковую струк-
структуру и состоят из следующих полей:
□ флаг Present— если он установлен, значит, соответствующая страница
(или Таблица Страниц) содержится в основной памяти. Если флаг сбро-
шен, значит, страница не содержится в основной памяти, и остальные би-
биты записи могут быть использованы операционной системой по ее усмот-
усмотрению. Если запись Таблицы Страниц или каталога страниц, участвующих
в преобразовании адреса, содержит сброшенный флаг Present, блок
управления страницами сохраняет линейный адрес в управляющем реги-
регистре сг2 и генерирует исключение "ошибка обращения к странице".
(В главе 17 мы увидим, как Linux использует это поле);
□ поле, содержащее 20 старших битов физического адреса страничного кад-
кадра — поскольку каждый страничный кадр имеет размер 4 Кбайт, его фи-
физический адрес должен быть кратен 4096, и, следовательно, 12 младших
битов этого физического адреса всегда содержит 0. Если это поле относит-
относится к каталогу страниц, значит, страничный кадр содержит Таблицу Стра-
Страниц; если же оно относится к Таблице Страниц, страничный кадр содер-
содержит страницу с данными;
□ флаг Accessed — устанавливается всякий раз, когда блок управления адре-
адресует соответствующий страничный кадр. Этот флаг может б>ыть использо-
использован операционной системой, когда она выбирает страницы для выгрузки
на диск. Блок управления никогда не сбрасывает этот флаг; сброс должна
делать операционная система;
□ флаг Dirty— относится только к записям Таблицы Страниц. Флаг уста-
устанавливается всякий раз, когда выполняется операция записи в страничный
кадр. Как и флаг Accessed, флаг Dirty может быть использован операцион-
ной системой, когда она выбирает страницы для выгрузки на диск. Блок
управления никогда не сбрасывает этот флаг; сброс должна делать опера-
операционная система;
□ флаг Read/Write — сообщает о правах доступа (чтение/запись или чтение)
к странице или к Таблице Страниц (см. разд. "Аппаратная схема защиты "
далее в этой главе);
П флаг user/Supervisor— определяет уровень привилегий, необходимый
для обращения к странице или к Таблице Страниц;
□ флаги pcd и pwt — определяют способ, которым аппаратный кэш управля-
управляет страницей или Таблицей Страниц;
□ флаг Page size — относится только к записям каталога страниц. Если флаг
установлен, значит, запись ссылается на страничный кадр длиной 2 или
4 Мбайт;
□ флаг Global — относится только к записям Таблицы Страниц. Этот флаг
впервые появился в модели Pentium Pro, чтобы не допустить сброса часто
используемых страниц из TLB-кэша (см. разд. "Буферы быстрого преоб-
преобразования адреса (TLB)" далее в этой главе). Флаг действует, только если
установлен флаг pge (Page Global Enable, Разрешение атрибута глобально-
глобальности) в регистре сг4.
Расширенное управление страницами
Начиная с модели Pentium, микропроцессоры 80x86 могут использовать рас-
расширенное управление страницами, которое позволяет страничным кадрам
иметь размер 4 Мбайт, а не 4 Кбайт (рис. 2.8). Расширенное управление стра-
страницами применяется для преобразования больших интервалов смежных ли-
линейных адресов в соответствующие интервалы физических адресов. В таких
случаях ядро может обойтись без промежуточных Таблиц Страниц, экономя,
таким образом, память и сохраняя записи TLB-буферов.
Рис. 2.8. Расширенное управление страницами
Как было замечено в предыдущем разделе, расширенное управление страни-
страницами делит 32-битовый линейный адрес на два поля:
□ каталог (Directory) — старшие 10 битов;
□ смещение (offset) — младшие 22 бита.
При расширенном управлении страницами записи каталога страниц такие же,
как и при нормальном, за исключением следующих двух моментов:
□ флаг Page size должен быть установлен;
□ только 10 старших битов 20-битового поля с физическим адресом являют-
являются значимыми. Дело в том, что каждый физический адрес выравнивается
по границе 4 Мбайт, так что 22 младших бита адреса содержат нули.
Расширенное управление страницами сосуществует с обычным; оно включа-
включается установкой флага pse в процессорном регистре сг4.
Аппаратная схема защиты
Блок управления страницами применяет иную схему защиты, чем блок сег-
сегментации. В то время как в процессорах 80x86 существует четыре уровня
привилегий при обращении к сегменту, только два уровня привилегий
ассоциировано со страницами и Таблицами Страниц. Дело в том, что при-
привилегии определяются при помощи флага user/supervisor, упомянутого в
разд. "Обычное управление страницами". Когда этот флаг сброшен, обра-
обращаться к странице возможно, лишь если текущий уровень привилегий мень-
меньше 3 (для Linux это равносильно тому, что процессор работает в режиме яд-
ядра). Когда этот флаг установлен, к странице можно обратиться при любом
уровне привилегий.
Кроме того, вместо трех типов прав доступа (чтение, запись и выполнение),
ассоциированных с сегментами, со страницами ассоциировано только два
(чтение и запись). Если флаг Read/write в записи каталога страниц или Таб-
Таблицы Страниц сброшен, соответствующая Таблица Страниц или страница
может быть только прочитана. В противном случае в отношении нее допус-
допустимы и чтение, и запись2.
Пример обычного управления страницами
Простой пример поможет понять, как работает обычное управление страни-
страницами. Предположим, что ядро присвоило работающему процессу пространст-
пространство линейных адресов с 0x20000000 по 0x2003ffff3. Это пространство содер-
содержит ровно 64 страницы. Нас не заботят физические адреса страничных кад-
кадров, включающих в себя эти страницы. На самом деле, некоторые из них
вообще могут быть за пределами основной памяти. Нас интересуют осталь-
остальные поля записей Таблицы Страниц.
Начнем с десяти старших битов линейных адресов, которые интерпретиру-
интерпретируются блоком управления страницами как поле Directory. Адреса начинаются
с двойки, за которой следуют нули, так что во всех случаях эти 10 битов
2 В последних моделях процессоров Pentium 4 имеется флаг NX (No eXecute, "не выполнять")
у каждой 64-битовой записи Таблицы Страниц (механизм расширения физических адресов должен
быть включен; Linux 2.6.11 и выше поддерживают эту аппаратную возможность.
3 Как мы увидим в следующих главах, пространство линейных адресов размером в 3 Гбайт являет-
является верхним пределом, но процессу режима пользователя разрешено ссылаться только на некоторое
его подмножество.
имеют одно значение 0x080, или 128 в десятичной системе счисления. Полу-
Получается, что поле Directory во всех адресах ссылается на 129 запись каталога
страниц, принадлежащего процессу. Эта запись должна содержать физиче-
физический адрес Таблицы Страниц, присвоенной процессу (рис. 2.9). Если процес-
процессу не присвоены никакие другие линейные адреса, остальные 1023 записи
каталога страниц заполнены нулями.
Рис. 2.9. Пример управления страницами
Значения, которые мы предполагаем увидеть в средних десяти битах (то есть
значения полей Table), лежат в диапазоне от 0 до 0x03f, или от 0 до 63. То
есть только первые 64 записи Таблицы Страниц имеют смысл. Остальные 960
содержат одни нули.
Предположим, процессу нужно прочитать байт с линейным адресом
0x20021406. Устройство управления страницами обрабатывает этот адрес
следующим образом:
□ Поле Directory со значением 0x80 используется для выбора записи 0x80
в каталоге страниц. Запись указывает на Таблицу Страниц, ассоциирован-
ассоциированную с процессом.
□ Поле Table со значением 0x21 используется для выбора записи 0x21 в
Таблице Страниц. Запись указывает на страничный кадр, содержащий
нужную страницу.
□ Наконец, поле offset со значением 0x406 используется для выбора байта
со смещением 0x406 в нужном страничном кадре.
Если флаг Present у записи 0x21 в Таблице Страниц сброшен, значит, стра-
страница отсутствует в памяти. В таком случае блок управления страницами вы-
выдает исключение "ошибка обращения к странице" при преобразовании ли-
линейного адреса. То же исключение генерируется, когда процесс пытается об-
обратиться к линейному адресу за пределами интервала, ограниченного
адресами 0x20000000 и 0x2003ffff, потому что записи Таблицы Страниц, не
ОТНОСЯЩИеСЯ К Процессу, Заполнены НуЛЯМИ; В чаСТНОСТИ, флаги Present
сброшены.
Механизм расширения
физических адресов (РАЕ)
Объем оперативной памяти, поддерживаемой процессором, ограничен коли-
количеством адресных контактов, соединенных с адресной шиной. В старых мо-
моделях процессоров Intel, от 80386 до Pentium, использовались 32-разрядные
физические адреса. Теоретически в таких системах могла быть установлена
оперативная память объемом 4 Гбайт, но на практике из-за требований,
предъявляемых процессами режима пользователя к пространству линейных
адресов, ядро не могло напрямую адресовать более 1 Гбайт оперативной па-
памяти, в чем мы убедимся в разд. "Управление страницами в Linux11 далее в
этой главе.
Однако большим серверам, на которых одновременно выполняются сотни, а
то и тысячи процессов, необходимо больше 4 Гбайт оперативной памяти, и в
последние годы на компанию Intel оказывалось сильное давление с требова-
требованием расширить объем памяти, поддерживаемой в 32-разрядной архитекту-
архитектуре 80x86.
Компания удовлетворила эти запросы, увеличив количество адресных кон-
контактов на процессорах с 32 до 36. Начиная с модели Pentium Pro, все процес-
процессоры Intel способны адресовать до 236 = 64 Гбайт оперативной памяти. Одна-
Однако увеличившийся диапазон физических адресов можно эксплуатировать
только с помощью нового механизма управления памятью, который преобра-
преобразует 32-битовые линейные адреса в 36-битовые физические.
Одновременно с процессором Pentium Pro компания Intel представила меха-
механизм, названный расширением физических адресов, или РАЕ (Physical
Address Extension). Другой механизм, расширение размера страницы, PSE-36
(Page Size Extension), был представлен в процессоре Pentium III, но в Linux он
не применяется, и мы не станем обсуждать его в этой книге.
Расширение физических адресов активизируется с помощью флага рае в
управляющем регистре сг4. Флаг ps (Page Size) в записи каталога страниц
позволяет использовать страницы большого размера B Мбайт при включен-
включенном механизме РАЕ).
Для поддержки механизма РАЕ компания Intel изменила схему управления
страницами:
□ оперативная память F4 Гбайт) разбита на 224 страничных кадра, а размер
поля физических адресов в записях Таблицы Страниц увеличен с 20
до 24 битов. Поскольку при включенном механизме РАЕ запись Таблицы
Страниц должна иметь 12 флаговых битов (описанных в разд. "Обычное
управление страницами") и 24 бита физического адреса, для обеспечения
36 битов пришлось удвоить размер записи Таблицы Страниц, доведя его
до 64 битов. В результате Таблица Страниц объемом в 4 Кбайт при рабо-
работающем механизме РАЕ содержит 512 записей вместо 1024;
□ был введен новый уровень Таблиц Страниц, названный таблицей указате-
указателей на каталог страниц, PDPT (Page Directory Pointer Table). Эта таблица
состоит из четырех 64-битовых записей;
□ управляющий регистр сгЗ имеет 27-битовое поле базового адреса таблицы
указателей на каталог страниц. Поскольку таблица PDPT хранится в пер-
первых четырех гигабайтах оперативной памяти и выровнена по границе
32 байта B5), 27 битов достаточно для представления ее базового адреса;
□ при отображении линейных адресов в 4-килобайтовые страницы (флаг ps
сброшен в записи каталога страниц), 32 бита линейного адреса интерпре-
интерпретируются следующим образом:
• сгЗ — указывает на таблицу PDPT;
• биты 31—30 — указывают на одну из четырех записей таблицы PDPT;
• биты 29—21 — указывают на одну из 512 записей каталога страниц;
• биты 20—12 — указывают на одну из 512 записей Таблицы Страниц;
• биты 11—0 — смещение внутри 4-килобайтовой страницы;
□ при отображении линейных адресов в 2-мегабайтовые страницы (флаг ps
установлен в записи каталога страниц), 32 бита линейного адреса интер-
интерпретируются следующим образом:
□ сгЗ — указывает на таблицу PDPT;
□ биты 31—30 — указывают на одну из четырех записей таблицы PDPT;
□ биты 29—21 — указывают на одну из 512 записей каталога страниц;
□ биты 20—0 — смещение внутри 2-мегабайтовой страницы.
Подведем итоги. Когда регистр сгЗ установлен, есть возможность адресовать
до 4 Гбайт оперативной памяти. При необходимости адресовать больший
объем памяти нам придется занести новое значение в регистр сгЗ или изме-
изменить содержимое таблицы указателей на каталог страниц. Однако главная
проблема, связанная с механизмом РАЕ, заключается в том, что линейные
адреса по-прежнему имеют длину 32 бита. Это вынуждает разработчиков яд-
ядра использовать одни и те же линейные адреса для отображения разных об-
областей оперативной памяти. Мы вкратце опишем, как Linux инициализирует
Таблицы Страниц при включенном механизме РАЕ далее в этой главе. Оче-
Очевидно, что механизм РАЕ не увеличивает пространство линейных адресов
процесса, потому что имеет отношение только к физическим адресам. Кроме
того, только ядро может модифицировать таблицы страниц процесса, т. е.
процесс, работающий в пользовательском режиме, не может иметь адресное
пространство, превышающее 4 Гбайт. С другой стороны, механизм РАЕ по-
позволяет ядру эксплуатировать до 64 Гбайт оперативной памяти, тем самым
значительно увеличивая количество процессов в системе.
Управление страницами
в 64-разрядных архитектурах
Как мы видели в предыдущих разделах, в 32-разрядных микропроцессорах
обычно применяется двухуровневая схема управления страницами4. Однако
двухуровневое управление страницами не подходит для компьютеров с
64-разрядной архитектурой. Чтобы понять, почему это так, проведем мыс-
мысленный эксперимент.
Пусть для начала страница имеет стандартный размер 4 Кбайт. Поскольку
1 Кбайт покрывает интервал из 210 адресов, 4 Кбайт вмещает в себя
212 адресов, и поле offset имеет длину 12 битов. Остается 52 бита линейного
адреса на поля Table и Directory. Если мы решим использовать только 48 из
64 битов для адресации (это ограничение создает нам комфортное адресное
пространство в 256 Тбайт!), оставшиеся 48 - 12 = 36 битов следует поделить
между полями Table и Directory. Отведя под каждое поле 18 битов, мы полу-
получим, что каталог страниц и Таблицы Страниц каждого процесса будут содер-
содержать по 218 записей, т. е. более 256 000.
По этой причине все системы аппаратного управления страницами для
64-разрядных процессоров используют дополнительные уровни управления
страницами. Количество используемых уровней зависит от типа процессора.
В табл. 2.4 дана сводка основных характеристик систем аппаратного управ-
управления памятью, используемых на некоторых 64-разрядных платформах, под-
поддерживаемых операционной системой Linux. Краткое описание аппаратной
части этих платформ приведено в главе 1.
Таблица 2А. Уровни управления страницами в 64-разрядных архитектурах
Название I Размер I I^SS? I ^^^"I M«
платформы страницы ^JJJ"* 2™™™™ линейного адреса
alpha 8 Кбайт 43 3 10 + 10 + 10 + 13
ia64 4 Кбайт 39 3 9 + 9 + 9 + 12
4 Третий уровень управления страницами в процессорах 80x86 при включенном механизме РАЕ
был введен лишь для того, чтобы уменьшить количество записей в каталоге страниц и Таблицах
Страниц с 1024 до 512. Это позволяет увеличить размер записи в Таблице Страниц с 32 до 64 битов
с тем, чтобы они могли вмещать 24 старших бита физического адреса.
Таблица 2.4 (окончание)
Название I Размер I К«™<> I «>»%%%?« I Обивка
платформы страницы dfl^"blA страницами линейного адреса
ррс64 4 Кбайт 41 3 10 + 10 + 9 + 12
sh64 4 Кбайт 41 3 10 + 10 + 9 + 12
х86_64 4 Кбайт 48 4 9 + 9 + 9 + 9 + 12
Как мы увидим в разд. "Управление страницами в Linux" далее в этой главе,
операционной системе Linux удалось подобрать общую модель управления
страницами, которая годится для большинства поддерживаемых систем ап-
аппаратного управления страницами.
Аппаратный кэш
Тактовая частота современных микропроцессоров измеряется в гигагерцах, а
время доступа чипов динамической оперативной памяти находится в преде-
пределах сотен тактовых циклов. Это означает, что процессор сильно задерживает-
задерживается при выполнении инструкций, требующих выборки операндов из памяти
и/или сохранения результатов в памяти.
Аппаратные кэши были созданы для сглаживания несоответствия в скорости
работы между процессором и оперативной памятью. Они основаны на хоро-
хорошо известном принципе локальности, который справедлив как для программ,
так и для данных. Он заключается в том, что в силу циклической структуры
программ и размещения их данных в линейных массивах, адреса, близкие к
тем, что были использованы недавно, имеют высокую вероятность использо-
использования в ближайшем будущем. Поэтому имеет смысл использовать компакт-
компактную и более быструю память для хранения недавно использованного кода и
данных. С этой целью в архитектуре 80x86 был создан новый блок, называе-
называемый строкой (line). Он состоит из нескольких десятков смежных байтов, ко-
которые передаются в монопольном режиме между медленной динамической
оперативной памятью и быстрой статической оперативной памятью, приме-
применяемой для реализации кэшей.
Кэш разбивается на подмножества строк. В одном предельном случае кэш
может быть отображен напрямую, и в этом случае строка основной памяти
всегда хранится строго в одном месте кэша. В другом случае кэш может быть
полностью ассоциативным. Это означает, что любая строка памяти может
храниться в любом месте кэша. Однако большинство кэшей, в определенной
степени, являются множественно-ассоциативными с п каналами, т. е. любая
строка основной памяти может быть сохранена в любой из п строк кэша. На-
пример, строка памяти может быть сохранена в двух разных строках множе-
множественно-ассоциативного кэша с двумя каналами.
Как показано на рис. 2.10, кэш помещается между блоком управления стра-
страницами и основной памятью. Он состоит из памяти аппаратного кэша и кон-
контроллера кэша. Память кэша содержит строки памяти. Контроллер кэша со-
содержит массив записей, по одной на каждую строку памяти кэша. Каждая
запись включает в себя тег и несколько флагов, описывающих статус строки
кэша. Тег содержит несколько битов, позволяющих контроллеру распознать
область памяти, отображаемую строкой кэша в данный момент. Биты физи-
физического адреса памяти обычно разбиваются на три группы: старшие биты со-
соответствуют тегу, средние— индексу подмножества контроллера кэша, а
младшие — смещению внутри строки.
Рис. 2.10. Аппаратный кэш процессора
При обращении к ячейке оперативной памяти процессор извлекает индекс
подмножества из физического адреса и сравнивает теги всех строк в подмно-
подмножестве со старшими битами физического адреса. Если строка, у которой тег
совпадает со старшими битами адреса, найдена, говорят, что процессор попал
в кэш; в противном случае процессор промахнулся.
Когда имеет место попадание в кэш, контроллер кэша ведет себя в зависимо-
зависимости от типа доступа. В случае операции чтения контроллер выбирает данные
из строки кэша и пересылает их в регистр процессора. Обращения к опера-
оперативной памяти не происходит, и время работы процессора экономится. Соб-
Собственно, поэтому и был изобретен кэш. Для операции записи контроллер мо-
может прибегнуть к одной из двух базовых стратегий: сквозная запись и обрат-
обратная запись. При сквозной записи контроллер записывает данные как в
оперативную память, так и в строку кэша, фактически выключая кэш для
операций записи. При обратной записи достигается немедленный эффект,
поскольку только строка кэша обновляется, а содержимое оперативной памя-
памяти не меняется. Естественно, оперативная память тоже, в конце концов,
должна быть обновлена. Контроллер кэша записывает строку кэша обратно в
оперативную память, только когда процессор выполняет инструкцию, тре-
требующую сброса записей кэша, или когда генерируется аппаратный сигнал
FLUSH (как правило, после промаха мимо кэша).
Если произошел промах мимо кэша, строка кэша в случае необходимости
записывается в память, а в запись кэша заносится корректная строка из опе-
оперативной памяти.
В многопроцессорных системах у каждого процессора есть отдельный аппа-
аппаратный кэш, и, следовательно, требуется дополнительная электронная схема
для синхронизации содержимого кэшей. Как видно из рис. 2.11, каждый про-
процессор имеет свой аппаратный кэш. Однако сейчас обновление отнимает
больше времени. Как только процессор модифицирует свой аппаратный кэш,
он должен проверить, хранятся ли те же данные в остальных аппаратных кэ-
кэшах. Если хранятся, он должен известить те процессоры о необходимости
обновления их кэшей соответствующими значениями. Такая деятельность
часто называется отслеживанием кэшей. К счастью, все делается на аппарат-
аппаратном уровне и не имеет никакого отношения к ядру.
Рис. 2.11. Кэши в двухпроцессорной системе
В настоящее время технология кэшей бурно развивается. Например, первые
модели Pentium включали в себя единственный кэш, который назывался
L1-кэшем. Последующие модели имели также более крупные и более мед-
медленные кэши: Ь2-кэш, ЬЗ-кэш и т. д. Согласованность между уровнями кэшей
достигается аппаратно. Linux игнорирует эти аппаратные подробности и
предполагает наличие одного кэша.
Флаг со процессорного регистра его используется для включения или выклю-
выключения кэша. Флаг ш в том же регистре определяет, какая стратегия исполь-
используется для кэшей — сквозная запись или обратная запись.
Еще одна интересная особенность кэша Pentium состоит в том, что он позво-
позволяет операционной системе ассоциировать с каждым страничным кадром ин-
индивидуальную политику управления кэшем. Для этой цели каждая запись ка-
каталога страниц и каждая запись Таблицы Страниц содержит два флага: pcd
(Page Cache Disable, отключить кэш страниц), который определяет, должен ли
кэш быть включен или выключен при обращении к данным в страничном
кадре, и pwt (Page Write-Through, Сквозная запись страницы), который опре-
определяет выбор стратегии при записи данных в страничный кадр. Linux сбрасы-
сбрасывает флаги pcd и pwt во всех записях каталогов страниц и Таблиц Страниц, и в
результате включается кэширование всех страничных кадров и во всех случа-
случаях применяется стратегия обратной записи.
Буферы быстрого преобразования адреса (TLB)
Помимо аппаратных кэшей общего назначения у процессоров 80x86 есть еще
один кэш, называемый буфером быстрого преобразования адреса (Translation
Lookaside Buffer, TLB) и предназначенный для ускорения преобразования
линейных адресов. Когда линейный адрес используется впервые, соответст-
соответствующий физический адрес вычисляется с использованием медленных опера-
операций обращения к Таблицам Страниц в оперативной памяти. Затем физиче-
физический адрес сохраняется в записи TLB-буфера, чтобы последующие ссылки на
тот же линейный адрес разрешались быстрее.
В многопроцессорных системах каждый процессор имеет собственный TLB-
буфер, называемый локальным. В отличие от аппаратного кэша, записи в
TLB-буферах не нуждаются в синхронизации, потому что процессы, рабо-
работающие на разных процессорах, могут ассоциировать один линейный адрес
с разными физическими адресами.
Когда изменяется содержимое управляющего регистра сгЗ у какого-то про-
процессора, аппаратная часть автоматически делает недействительными все
записи в локальном TLB-буфере, потому что используется новый набор таб-
таблиц страниц, а TLB-буферы указывают на устаревшие данные.
Управление страницами в Linux
В Linux принята универсальная модель управления страницами, которая под-
подходит как к 32-разрядным, так и к 64-разрядным архитектурам. Как было ска-
сказано ранее в разд. "Управление страницами в 64-разрядных архитектурах",
для 32-разрядных архитектур достаточно двух уровней управления страни-
страницами, а 64-разрядные требуют большего количества уровней. Вплоть до вер-
версии 2.6.10, модель управления страницами в Linux состояла из трех уровней.
Начиная с версии 2.6.11, была принята четырехуровневая модель5. Соответ-
Соответствующие четыре типа таблиц страниц изображены на рис. 2.12 и называют-
называются так:
□ глобальный каталог страниц;
□ верхний каталог страниц;
□ средний каталог страниц;
□ Таблица Страниц.
Глобальный каталог страниц включает в себя адреса нескольких верхних ка-
каталогов страниц, которые, в свою очередь, содержат адреса нескольких сред-
средних каталогов страниц, а те — адреса нескольких Таблиц Страниц. Каждая
запись Таблицы Страниц указывает на страничный кадр. Таким образом, ли-
линейный адрес можно разбить на пять частей. На рис. 2.12 не показаны номера
битов, потому что размер каждой части зависит от конкретной архитектуры.
Рис. 2.12. Модель управления страницами в Linux
Для 32-разрядных архитектур без расширения физических адресов достаточ-
достаточно двух уровней управления страницами. Linux фактически исключает из мо-
модели поля верхнего и среднего каталогов страниц, считая, что эти поля со-
содержат нулевые биты. Тем не менее позиции верхнего и среднего каталогов
страниц в цепочке указателей сохраняются, чтобы один и тот же код работал
5 Это нововведение призвано обеспечить полную поддержку разбиения линейного адреса, которое
принято на платформе х86_64.
как на 32-разрядной, так и на 64-разрядной архитектуре. Ядро сохраняет по-
позиции верхнего каталога страниц и среднего каталога страниц, устанавливая
количество записей в них 1 и отображая эти записи в соответствующую
запись глобального каталога страниц.
В 32-разрядных архитектурах при включенном механизме расширения физи-
физических адресов применяется трехуровневое управление страницами. Гло-
Глобальный каталог страниц в Linux соответствует таблице указателей на ката-
каталог страниц в модели управления страницами 80x86, верхний каталог стра-
страниц отсутствует, средний каталог страниц соответствует каталогу страниц, а
Таблица Страниц Linux соответствует Таблице Страниц 80x86.
Наконец, в 64-разрядных архитектурах используются три или четыре уровня
управления страницами, в зависимости от того, как аппаратная часть разби-
разбивает линейный адрес (см. табл. 2.2).
Работа операционной системы Linux с процессами сильно зависит от подхода
к управлению страницами. Например, автоматическое преобразование ли-
линейных адресов в физические позволяет решать следующие задачи:
□ присваивать разные пространства физических адресов разным процессам,
что послужит эффективной защитой от ошибок адресации;
□ проводить различие между страницами (блоками данных) и страничными
кадрами (физическими адресами в основной памяти). Это позволяет запи-
записать на диск страницу, хранящуюся в одном страничном кадре, а потом за-
загрузить ее в другой страничный кадр. Подобный подход лежит в основе
механизма виртуальной памяти (см. главу 17).
В остальных разделах этой главы мы, ради конкретности изложения, будем
иметь в виду схему управления страницами, используемую в процессорах
80x86.
Как мы увидим в главе 9, каждый процесс имеет собственный глобальный
каталог страниц и собственный набор Таблиц Страниц. Когда происходит
переключение процессов (см. разд. "Переключение процессов" главы 3), Linux
сохраняет содержимое управляющего регистра сгЗ в дескрипторе выполняв-
выполнявшегося процесса, а затем загружает в регистр сгЗ значение, хранящееся в де-
дескрипторе процесса, который будет выполняться далее. Таким образом, когда
процесс возобновит свою работу, блок управления страницами обратится к
корректному набору Таблиц Страниц.
Отображение линейных адресов в физические превращается в механическую
задачу, хотя она и остается довольно сложной. Следующие несколько разде-
разделов этой главы представляют довольно однообразный список функций и мак-
макросов, которые предоставляют ядру информацию, необходимую ему при по-
поиске адресов и работе с таблицами, причем большинство функций состоит из
одной или двух строк. Возможно, читатель поначалу ограничится лишь бег-
беглым просмотром этих разделов, но понимать назначение описанных здесь
макросов полезно, потому что мы часто будем упоминать их в других главах
этой книги.
Поля линейного адреса
Следующие макросы упрощают работу с Таблицей Страниц:
□ pageshift — задает длину поля offset в битах. В архитектуре 80x86 воз-
возвращает число 12. Поскольку все адреса в странице должны помещаться в
поле offset, размер страницы в системах 80x86 составляет 212, или знако-
знакомые нам 4096 байтов. Таким образом, значение 12 для макроса pageshift
можно считать логарифмом по основанию 2 от общего размера страницы.
Этот макрос используется макросом pagesize, возвращающим размер
страницы. Кроме того, макрос pagemask возвращает значение Oxf f f f f ooo и
используется для маскировки всех битов поля offset;
□ pmdshift — суммарная длина в битах полей offset и Table линейного ад-
адреса. Иными словами, это логарифм размера области, которую может ото-
отобразить средний каталог страниц;
□ pmdsize— вычисляет размер области, отображаемой одной записью
среднего каталога страниц, т. е. Таблицей Страниц;
□ PMDMASK СЛуЖИТ ДЛЯ МаСКИрОВКИ ВСеХ биТОВ ПОЛеЙ Offset И Table.
Когда механизм расширения физических адресов выключен, макрос
pmdshift возвращает значение 22 A2 для offset плюс 10 для таЫе),
pmdsize возвращает 222, или 4 Мбайт, а макрос pmdmask— значение
Oxf fcooooo. Если же механизм РАЕ включен, макрос pmdshift возвращает
значение 22 A2 для offset плюс 12 для Table), pmdsize возвращает 221,
или 2 Мбайт, а макрос pmd_mask — значение Oxf fеооооо.
В страницах большого размера последний уровень таблиц страниц не ис-
используется. Поэтому макрос largepagesize, который возвращает размер
большой страницы, равен pmdsize, а макрос largepagemask, который
маскирует все биты полей offset и Table в адресе большой страницы, ра-
равен pmd_mask;
□ pudshift — определяет логарифм размера области, которую может ото-
отобразить запись верхнего каталога страниц;
□ pudsize— вычисляет размер области, отображаемой одной записью
верхнего каталога страниц;
□ PUD_MASK СЛуЖИТ ДЛЯ МаСКИрОВКИ ВСеХ биТОВ ПОЛеЙ Offset, TableMiddle
Dir И Upper Dir.
8 архитектуре 80x86 макрос pudshift всегда равен pmdshift, а макрос
pudsize возвращает 4 Мбайт или 2 Мбайт;
□ pgdirshift— определяет логарифм размера области, которую может
отобразить запись верхнего каталога страниц;
□ pgdirsize— вычисляет размер области, отображаемой одной записью
верхнего каталога страниц;
□ pgdirmask — служит для маскировки всех битов полей offset,
TableMiddle Dir И Upper Dir.
Когда механизм расширения физических адресов выключен, макрос
pgdirshift возвращает значение 22 (то же, что и макросы pmdshift и
pudshift), макрос pgdirsize возвращает 222 D Мбайт), а макрос
pgdir_mask— Oxffcooooo. Если же механизм РАЕ включен, макрос
pgdirshift возвращает значение 30 A2 для offset плюс 9 для Table плюс
9 для Middle Dir), pgdirsize возвращает230 A Гбайт), а макрос
pgdir_mask — значение Охсооооооо;
□ PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD И PTRS_PER_PGD ВЫЧИСЛЯЮТ КО-
личество записей в Таблице Страниц, а также в среднем, верхнем и гло-
глобальном каталогах страниц. При выключенном механизме РАЕ возвраща-
возвращают, соответственно, 1024, 1, 1 и 1024, а при включенном — 512, 512, 1 и 4.
Работа с Таблицей Страниц
Типы ptet, pmdt, pudt и pgdt описывают формат записи Таблицы Стра-
Страниц, среднего каталога страниц, верхнего каталога страниц и глобального
каталога страниц соответственно. Они представляют собой 64-битовые типы
данных при включенном механизме РАЕ и 32-битовые при выключенном.
Тип pgprott является 64-битовым (механизм РАЕ включен) или 32-битовым
(механизм РАЕ выключен) и представляет флаги, ассоциированные с одной
записью.
Пять макросов преобразования типа, pte, _pmd, pud, pgd и pgprot,
переводят целое без знака в требуемый тип. Пять других макросов, pteval,
pmdval, pudval, pgdval и pgprotval, выполняют обратное преобразование
из упомянутых специализированных типов в целое без знака.
Ядро также предоставляет ряд макросов и функций для чтения и модифика-
модификации записей таблицы страниц:
□ макросы pte_none, pmd_none, pud_none И pgd_none — возвращают 1, если
соответствующая запись содержит нули, и 0 в противном случае;
□ макросы pte_clear, pmd_clear, pud__clear И pgd_clear— очищают запись
соответствующей таблицы страниц, тем самым запрещая процессу ис-
пользовать линейные адреса, отображаемые этой записью. Функция
ptepgetandciear о очищает запись Таблицы Страниц и возвращает пре-
предыдущее значение;
□ макросы setpte, setpmd, setpud и set jpgd — записывают заданное зна-
значение в таблицу страниц. Макрос setpteatomic идентичен макросу
setpte, но при включенном механизме РАЕ он гарантирует, что 64-би-
64-битовое значение будет записано атомарным образом;
□ функция pte_same(a,b) — возвращает 1, если две записи Таблицы Стра-
Страниц, а и ь, ссылаются на одну и ту же страницу и задают одинаковые при-
привилегии. В противном случае возвращается 0;
□ функция pmdiarge(e) — возвращает 1, если запись е среднего каталога
страниц ссылается на страницу большого размера B или 4 Мбайт). В про-
противном случае возвращается 0.
Макрос pmdbad используется в различных функциях для проверки коррект-
корректности записей среднего каталога страниц, передаваемых в качестве парамет-
параметров. Он возвращает 1, если запись указывает на недопустимую Таблицу
Страниц, т. е. если имеет место хотя бы одно из следующих условий:
□ страница отсутствует в основной памяти (флаг Present сброшен);
□ разрешено только чтение страницы (флаг Read/write сброшен);
П1 сброшен либо флаг Accessed, либо флаг Dirty (Linux принудительно уста-
устанавливает эти флаги для каждой имеющейся Таблицы Страниц).
Макросы pudbad и pgdbad всегда возвращают 0. Макроса ptebad не сущест-
существует, потому что вполне допустимо, чтобы запись Таблицы Страниц ссыла-
ссылалась на страницу, отсутствующую в основной памяти, не доступную для
записи, или вообще недоступную.
Макрос ptepresent возвращает 1, если либо флаг Present, либо флаг
Page size в записи Таблицы Страниц равняется 1. В противном случае мак-
макрос возвращает 0. Вспомним, что флаг Page size в записи Таблицы Страниц
не несет никакого смысла для блока управления страницами микропроцессо-
микропроцессора. Тем не менее ядро сбрасывает флаг Present и устанавливает флаг
Page size для страниц, присутствующих в памяти, но без привилегий на чте-
чтение, запись или выполнение. Таким образом, любое обращение к этим стра-
страницам провоцирует исключение "ошибка обращения к странице", поскольку
флаг Present сброшен, но ядро, проверив флаг Page size, может распознать,
что ошибка возникла не из-за отсутствия страницы.
Макрос pmdpresent возвращает 1, если флаг Present соответствующей запи-
записи установлен, т. е. если соответствующая страница или Таблица Страниц
загружена в оперативную память. Макросы pudpresent и pgdpresent всегда
возвращают 1.
Функции, перечисленные в табл. 2.5, опрашивают текущее значение любого
из флагов в записи Таблицы Страниц. За исключением функции ptefileo,
эти функции работают корректно только на тех записях Таблицы Страниц,
для которых макрос ptepresent возвращает 1.
Таблица 2.5. Функции чтения флагов страницы
Имя функции Описание
pte_user () Считывает флаг User/Supervisor
pte_read() Считывает флаг User/Supervisor (в системах с процессором
80x86 страницы не могут быть защищены от чтения)
pte_write () Считывает флаг Read/Write
pte_exec() Считывает флаг User/Supervisor (в системах с процессором
80x86 страницы не могут быть защищены от выполнения)
pte_dirty () Считывает флаг Dirty
pte_young () Считывает флаг Accessed
pte_file() Считывает флаг Dirty (когда флаг Present сброшен, а флаг
Dirty установлен, страница принадлежит нелинейному отобра-
отображению файла; см. главу 16)
Другая группа функций, приведенная в табл. 2.6, устанавливает значения
флагов в записи Таблицы Страниц.
Таблица 2.6. Функции установки флагов страницы
Имя функции Описание
mk_pte_huge () Устанавливает флаги Page Size и Present в записи
Таблицы Страниц
pte_wrprotect () Сбрасывает флаг Read/Write
pte_rdprotect () Сбрасывает флаг User/Supervisor
pte_exprotect () Сбрасывает флаг User/Supervisor
pte_mkwrite () Устанавливает флаг Read/Write
pte_mkread () Устанавливает флаг User/Supervisor
ptejnkexec () Устанавливает флаг User/Supervisor
pte_mkclean () Сбрасывает флаг Dirty
pte_mkdirty () Устанавливает флаг Dirty
pte_mkold () Сбрасывает флаг Accessed (делает страницу "старой")
Таблица 2.6 (окончание)
Имя функции Описание
pte_mkyoung () Устанавливает флаг Accessed (делает страницу "све-
"свежей")
pte_modify(p,v) Присваивает всем правам доступа в записи Таблицы
Страниц р заданное значение v
ptep_set_wrprotect () Аналогично pte_wrprotect (), но работает с указате-
указателем на запись Таблицы Страниц
ptep_set_access_f lags () Если флаг Dirty установлен, присваивает указанное
значение правам доступа к странице и вызывает функ-
функцию flush_tlb_page()
ptep_mkdirty() Аналогична pte_mkdirty (), но работает с указателем
на запись Таблицы Страниц
ptep_test_and_clear_dirty () Аналогична pte_mkclean (), но работает с указателем
на запись Таблицы Страниц и возвращает старое зна-
значение флага
ptep_test_and_clear_young () Аналогична pte_mkold (), но работает с указателем на
запись Таблицы Страниц и возвращает старое значе-
значение флага
Теперь мы обсудим макросы из табл. 2.7, которые упаковывают адрес стра-
страницы и набор флагов защиты в запись таблицы страниц или выполняют об-
обратную операцию по извлечению адреса страницы из записи в таблице стра-
страниц. Обратите внимание, что некоторые из этих макросов ссылаются на стра-
страницу с помощью линейного адреса так называемого дескриптора страницы
(см. разд. "Дескрипторы страниц" главы 8), а не линейного адреса самой
страницы.
Таблица 2.7. Макросы для работы с записями Таблицы Страниц
Имя макроса Описание
pgd_index (addr) Возвращает индекс (относительное положение) записи в
глобальном каталоге страниц, которая отображает
линейный адрес addr
pgdof f set (mm, addr) Принимает в качестве параметров адрес дескриптора
памяти cw (см. главу 9) и линейный адрес addr. Воз-
Возвращает линейный адрес записи в глобальном каталоге
страниц, которая соответствует адресу addr. Сам гло-
глобальный каталог страниц определяется с помощью ука-
указателя, хранящегося в дескрипторе памяти
Таблица 2.7 (продолжение)
Имя макроса Описание
pgd_offset_k(addr) Возвращает линейный адрес записи в глобальном ката-
каталоге ядра, которая соответствует адресу addr (см.
разд. "Таблицы страниц ядра" далее в этой главе)
pgd_page (pgd) Возвращает адрес дескриптора страницы страничного
кадра, содержащего верхний каталог страниц, на кото-
который ссылается запись pgd глобального каталога стра-
страниц. В двух- или трехуровневой модели управления
страницами этот макрос эквивалентен макросу
pud_page (), примененному к записи верхнего каталога
страниц
pud_of f set (pgd, addr) Принимает в качестве параметров указатель pgd на
запись глобального каталога страниц и линейный адрес
addr. Возвращает линейный адрес записи в верхнем
каталоге страниц, которая соответствует адресу addr.
В двух- или трехуровневой модели управления страни-
страницами этот макрос возвращает pgd, адрес записи в гло-
глобальном каталоге страниц
pud_j)age (pud) Возвращает линейный адрес среднего каталога стра-
страниц, на который ссылается запись pud верхнего катало-
каталога страниц. В двухуровневой модели управления стра-
страницами этот макрос эквивалентен макросу pmd_page (),
примененному к записи среднего каталога страниц
pmd_index (addr) Возвращает индекс (относительное положение) записи в
среднем каталоге страниц, которая отображает линей-
линейный адрес addr
pmd_offset (pud, addr) Принимает в качестве параметров указатель pud на
запись верхнего каталога страниц и линейный адрес
addr. Возвращает линейный адрес записи в среднем
каталоге страниц, которая соответствует адресу addr.
В двухуровневой модели управления страницами этот
макрос возвращает pud, адрес записи в глобальном
каталоге страниц
pmd_page (pmd) Возвращает адрес дескриптора страницы для Таблицы
Страниц, на которую ссылается запись pmd среднего
каталога страниц. В двухуровневой модели управления
страницами запись pmd фактически является записью
в глобальном каталоге страниц
mk_pte(p,prot) Принимает в качестве параметров адрес дескриптора
страницы р и группу прав доступа prot и создает соот-
соответствующую запись Таблицы Страниц
pte_index (addr) Возвращает индекс (относительное положение) записи в
Таблице Страниц, которая отображает линейный адрес
addr
Таблица 2.7 (окончание)
Имя макроса Описание
pte_of f setkernel (dir, addr) Возвращает линейный адрес Таблицы Страниц, которая
соответствует линейному адресу addr, отображенному
средним каталогом страниц dir. Используется только с
главными таблицами страниц ядра (см. разд. "Таблицы
страниц ядра" далее в этой главе)
pte_offset_map(dir, addr) Принимает в качестве параметров указатель dir на
запись среднего каталога страниц и линейный адрес
addr. Возвращает линейный адрес записи в Таблице
Страниц, которая соответствует адресу addr. Если Таб-
Таблица Страниц хранится в верхней памяти, ядро уста-
устанавливает временное отображение памяти (см.
разд. "Отображение ядром страничных кадров верхней
памяти" главы 8), которое должно быть отменено мак-
макросом pte_unmap. Макросы pte_offset_map_nested и
pte_unmap_nested идентичны, но используют разные
временные отображения памяти
ptepage(x) Возвращает адрес дескриптора страницы, на которую
ссылается запись х Таблицы Страниц
ptetopgoff (pte) Извлекает из записи pte Таблицы Страниц смещение в
файле, определяющее страницу, принадлежащую нели-
нелинейному отображению файла в память (см.
разд. "Нелинейные отображения в память" главы 16)
pgofftopte (offset) Заполняет данными запись Таблицы Страниц для стра-
страницы, принадлежащей нелинейному отображению фай-
файла в память
Последняя группа функций в этом длинном списке была разработана для уп-
упрощения создания и удаления записей в таблицах страниц.
В двухуровневой модели управления страницами создание и удаление записи
среднего каталога страниц является тривиальной задачей. Как было сказано
ранее в этом разделе, средний каталог страниц содержит единственную
запись, которая указывает на подчиненную Таблицу Страниц. Таким обра-
образом, запись среднего каталога страниц в то же время является и записью гло-
глобального каталога страниц. Однако при работе с Таблицами Страниц созда-
создание записи может оказаться более сложным делом, потому что Таблица
Страниц, для которой создается запись, может и не существовать. В подоб-
подобных случаях приходится выделять новый страничный кадр, заполнять его ну-
нулями и добавлять туда запись.
Если механизм расширения физических адресов включен, ядро прибегает к
трехуровневому управлению страницами. Когда ядро создает новый глобаль-
ный каталог страниц, оно одновременно выделяет четыре соответствующих
средних каталога страниц, которые освобождаются только вместе с глобаль-
глобальным каталогом страниц.
При двух- или трехуровневой модели управления страницами запись верхне-
верхнего каталога страниц всегда отображается на единственную запись глобально-
глобального каталога страниц.
Как обычно, описание функций, перечисленных в табл. 2.8, относится к архи-
архитектуре 80x86.
Таблица 2.8. Функции для выделения страниц
Имя функции Описание
pgd_alloc (mm) Выделяет новый глобальный каталог страниц.
Если механизм РАЕ включен, то выделяет три
соответствующих средних каталога страниц, ко-
которые отображают линейные адреса режима
пользователя. В архитектуре 80x86 аргумент mm
(адрес дескриптора памяти) игнорируется
pgd_free (pgd) Освобождает глобальный каталог страниц, рас-
расположенный по адресу pgd. Если механизм РАЕ
включен, то освобождает три соответствующих
средних каталога страниц, которые отображают
линейные адреса пользовательского режима.
В архитектуре 80x86 аргумент mm (адрес
дескриптора памяти) игнорируется
pud_alloc (mm, pgd, addr) В двух- или трехуровневой модели управления
страницами эта функция ничего не делает. Она
просто возвращает адрес записи pgd в глобаль-
глобальном каталоге страниц
pud_free(x) В двух- или трехуровневой модели управления
страницами этот макрос ничего не делает
pmd_alloc (mm, pud, addr) Функция определена, чтобы в трехуровневой мо-
модели управления страницами можно было выде-
выделить новый средний каталог страниц для линей-
линейного адреса addr. Если механизм РАЕ выключен,
функция просто возвращает входной параметр
pud, т. е. адрес записи в глобальном каталоге
страниц. Если механизм РАЕ включен, функция
возвращает линейный адрес записи в среднем
каталоге страниц, который отображает линейный
адрес addr. Аргумент mm игнорируется
pmd_fгее (х) Ничего не делает, потому что средние каталоги
страниц выделяются и освобождаются вместе с
соответствующим глобальным каталогом страниц
Таблица 2.8 (окончание)
Имя функции Описание
pte_alloc_map (ram, pmd, addr) Принимает в качестве параметров адрес записи
pmd в среднем каталоге страниц и линейный ад-
адрес addr и возвращает адрес записи в Таблице
Страниц, соответствующей адресу addr. Если
запись в среднем каталоге страниц содержит од-
одни нули, функция выделяет новую Таблицу Стра-
Страниц с помощью функции pte_alloc_one (). Если
выделение новой Таблицы Страниц прошло ус-
успешно, запись, соответствующая адресу addr,
инициализируется, а флаг User/Supervisor ус-
устанавливается. Если Таблица Страниц хранится в
верхней памяти, ядро устанавливает временное
отображение памяти (см. разд. "Отображение
ядром страничных кадров верхней памяти" гла-
главы 8), которое должно быть отменено макросом
pte_unraap
pte_alloc_kernel (ram, pmd, addr) Если запись pmd среднего каталога страниц, ас-
ассоциированная с адресом addr, содержит одни
нули, функция выделяет новую Таблицу Страниц.
Затем она возвращает линейный адрес записи в
Таблице Страниц, ассоциированной с адресом
addr. Используется только с главными таблицами
страниц ядра (см. разд. "Таблицы страниц ядра"
далее в этой главе)
pte_free(pte) Освобождает Таблицу Страниц, ассоциирован-
ассоциированную с указателем на дескриптор страницы pte
pte_free_kernel (pte) Эквивалентна функции pte_free(), но использу-
используется для главных таблиц страниц ядра
clear_page_range (mmu, start, end) Очищает содержимое таблиц страниц процесса с
линейного адреса start по линейный адрес end
путем пошагового освобождения Таблиц Страниц
процесса и очистки соответствующих записей в
среднем каталоге страниц
Схема разбивки физической памяти
На этапе инициализации ядро должно построить карту физических адресов,
определяющую, какие интервалы физических адресов будут использоваться
ядром, а какие недоступны (либо потому что они отображают память, совме-
совместно используемую аппаратными устройствами для ввода/вывода, либо по-
потому что соответствующие страничные кадры содержат данные BIOS).
Следующие страничные кадры ядро считает зарезервированными:
□ попадающие в недоступные интервалы физических адресов;
□ содержащие код ядра и инициализированные структуры данных.
Страница, содержащаяся в зарезервированном страничном кадре, ни в коем
случае не может быть выделена динамически или выгружена на диск.
В качестве общего правила можно утверждать, что ядро Linux располагается
в оперативной памяти, начиная с физического адреса 0x00100000, т. е. со
второго мегабайта. Общее количество необходимых страничных кадров зави-
зависит от конфигурации ядра. При типичной конфигурации ядро можно уме-
уместить менее чем в трех мегабайтах оперативной памяти.
Почему ядро не загружается, начиная с первого доступного мегабайта опера-
оперативной памяти? Дело в том, что приходится принимать во внимание некото-
некоторые особенности архитектуры персональных компьютеров. Например:
□ страничный кадр 0 используется системой BIOS для хранения аппаратной
конфигурации системы, определяемой на этапе самотестирования при
включении питания (POST, Power-On Self-Test). Кроме того, BIOS многих
портативных компьютеров записывает данные в эту страницу даже после
инициализации системы;
□ физические адреса с 0х000а0000 по OxOOOfffff обычно резервируются под
рабочие процедуры BIOS, а также для отображения внутренней памяти
графических карт ISA. Это знаменитая "дыра" между 640 Кбайт и 1 Мбайт
во всех IBM-совместимых персональных компьютерах: физические адреса
существуют, но они зарезервированы, и операционная система не может
использовать соответствующие страничные кадры;
□ в некоторых моделях компьютеров могут быть зарезервированы дополни-
дополнительные страничные кадры в пределах первого мегабайта. Например, в
IBM ThinkPad страничный кадр ОхаО отображается в страничный кадр
0x9f.
На раннем этапе начальной загрузки (см. приложение 1) ядро опрашивает
BIOS и узнает размер физической памяти. В современных моделях компью-
компьютеров ядро также вызывает одну из процедур BIOS, чтобы построить список
интервалов физических адресов и соответствующих им типов памяти.
Затем ядро ВЫПОЛНЯет функцию mchine_specificjnemoryjsetup(), которая
строит карту физических адресов (пример в табл. 2.9). Конечно, ядро строит
эту таблицу на базе списка BIOS, если он доступен. В противном случае оно
строит таблицу, следуя консервативному подходу, принятому по умолчанию:
все страничные кадры с номерами от 0x9f (lowmemsizeo) до 0x100
(highmemory) помечаются как зарезервированные.
Таблица 2.9. Пример карты физических адресов, предоставляемой системой BIOS
Начало Конец Тип
0x00000000 0x0009ffff Доступно для использования
0x000f0000 OxOOOfffff Зарезервировано
0x00100000 0x07feffff Доступно для использования
0x07ff0000 0x07ff2fff Данные ACPI
0x07ff3000 0x07ffffff ACPI NVS
OxffffOOOO Oxffffffff Зарезервировано
В табл. 2.9 приведена типичная конфигурация для компьютера с оперативной
памятью объемом 128 Мбайт. Диапазон физических адресов с 0x07ff0000
по 0x07ff2fff содержит информацию об аппаратных устройствах в системе,
записанную системой BIOS на этапе самотестирования. На этапе инициали-
инициализации ядро копирует эту информацию в соответствующую структуру данных
ядра, а затем считает эти страничные кадры годными к использованию. Фи-
Физические адреса с 0x07ff3000 no 0x07ffffff отображаются на микросхемы по-
постоянной памяти аппаратных устройств. Диапазон физических адресов, на-
начинающийся с адреса OxffffOOOO, помечен как зарезервированный, потому что
он аппаратно отображается на микросхему постоянной памяти BIOS (см.
приложение 1). Обратите внимание, что BIOS может и не предоставить ника-
никакой информации для некоторых диапазонов физических адресов (в таблице
это диапазон с 0х000а0000 по OxOOOeffff). На всякий случай, Linux предпола-
предполагает, что такие диапазоны использовать нельзя.
Ядро необязательно видит всю физическую память, о которой сообщила сис-
система BIOS. Например, ядро может адресовать только 4 Гбайт оперативной
памяти, если оно не было скомпилировано с поддержкой механизма РАЕ,
даже если фактически доступен больший объем физической памяти. Функция
setupmemory () вызывается сразу ВСЛед за функцией machine_specific_
memorysetup(). Она анализирует таблицу областей физической памяти и
инициализирует несколько переменных, которые описывают разбивку физи-
физической памяти ядра. Эти переменные перечислены в табл. 2.10.
Чтобы избежать загрузки ядра в группы несмежных страничных кадров,
Linux предпочитает пропустить первый мегабайт оперативной памяти. Оче-
Очевидно, что страничные кадры, не зарезервированные архитектурой персо-
персонального компьютера, будут использованы операционной системой Linux для
хранения динамически выделенных страниц.
На рис. 2.13 изображено, как Linux заполняет первые 3 Мбайт оперативной
памяти. Предполагается, что ядро занимает менее 3 Мбайт.
Таблица 2.10. Переменные, описывающие разбивку физической памяти ядра
Имя переменной Описание
num_physpages Номер самого верхнего используемого страничного кадра
totalram_pages Общее количество используемых страничных кадров
min_iow_pfn Номер первого используемого страничного кадра после образа
ядра в оперативной памяти
max_pfn Номер последнего используемого страничного кадра
max_iow_pfn Номер последнего страничного кадра, отображенного ядром на-
напрямую (нижняя память)
totalhigh_pages Общее количество страничных кадров, не отображенных ядром
напрямую (верхняя память)
highstart_pfn Номер первого страничного кадра, не отображенного ядром на-
напрямую
highend_pfn Номер последнего страничного кадра, не отображенного ядром
напрямую
Рис. 2.13. Первые 768 страничных кадров C Мбайт) в Linux 2.6
Символ text, который соответствует физическому адресу 0x00100000, обо-
обозначает первый байт кода ядра. Аналогично, конец кода ядра идентифициру-
идентифицируется символом etext. Данные ядра делятся на две группы: инициализиро-
инициализированные и неинициализированные. Инициализированные данные начинаются
сразу после байта etext и заканчиваются байтом edata. Неинициализиро-
Неинициализированные идут следом вплоть до байта end.
Символы на рисунке не определяются в исходном коде ядра; они создаются
во время его компиляции6.
6 Линейные адреса этих символов можно найти в файле System.map, который создается сразу после
компиляции ядра.
Таблицы страниц процесса
Пространство линейных адресов процесса разбивается на две части:
□ линейные адреса с Охоооооооо по Oxbfffffff доступны, когда процесс вы-
выполняется в режиме пользователя или режиме ядра;
□ линейные адреса с Охсооооооо по Oxffffffff доступны, когда процесс вы-
выполняется в режиме ядра.
Когда процесс работает в пользовательском режиме, он оперирует линейны-
линейными адресами, меньшими чем ОхсООООООО. Работая в режиме ядра, процесс вы-
выполняет код ядра и оперирует адресами, которые больше или равны
ОхсООООООО. Впрочем, в некоторых случаях ядро должно обращаться к про-
пространству линейных адресов пользовательского режима, чтобы прочитать
или записать данные.
Макрос pageoffset возвращает значение ОхсООООООО, являющееся смещени-
смещением в пространстве линейных адресов процесса, где "живет" ядро. В этой кни-
книге мы часто будем писать само это значение, а не макрос.
Содержимое первых записей глобального каталога страниц, которые отобра-
отображают линейные адреса ниже адреса ОхсООООООО (первые 768 записей при
включенном механизме расширения физических адресов или первые три
записи при включенном механизме РАЕ), зависит от конкретного процесса.
Остальные записи, напротив, должны быть одинаковыми для всех процессов
и совпадать с соответствующими записями главного глобального каталога
страниц ядра.
Таблицы страниц ядра
Ядро сопровождает собственный набор таблиц страниц, корнем которого яв-
является главный глобальный каталог страниц ядра. После инициализации сис-
системы этот набор таблиц страниц ни разу не используется напрямую ни про-
процессами, ни потоками ядра. Верхние записи главного глобального каталога
страниц ядра являются эталоном для соответствующих записей глобальных
каталогов страниц любого обычного процесса в системе.
В главе 8 мы объясним, как ядро добивается того, что изменения в главном
глобальном каталоге страниц ядра распространяются на глобальные каталоги
страниц, фактически используемых процессами.
Сейчас мы покажем, как ядро инициализирует свои собственные таблицы
страниц. Эта деятельность разбивается на два этапа. Дело в том, что сразу
после загрузки образа ядра в память процессор все еще работает в реальном
режиме, т. е. управление страницами не включено.
На первом этапе ядро создает ограниченное адресное пространство, вклю-
включающее в себя код и сегменты данных ядра, первоначальные Таблицы Стра-
Страниц и 128 Кбайт различных динамических структур данных. Этого мини-
минимального адресного пространства вполне хватает, чтобы разместить ядро в
оперативной памяти и инициализировать его важнейшие структуры.
На втором этапе ядро использует всю доступную оперативную память и
должным образом настраивает таблицы страниц. Рассмотрим более подроб-
подробно, как все происходит.
Временные Таблицы Страниц ядра
Временный глобальный каталог страниц инициализируется статически во
время компиляции ядра, а временные Таблицы Страниц инициализируются
ассемблерной функцией startup_32 (), определенной в файле arch/i386/kernel
/head.S. В дальнейшем мы больше не будем упоминать о верхнем и среднем
каталогах страниц, потому что они приравнены к записям глобального ката-
каталога страниц. Поддержка расширения физических адресов на этой стадии от-
отключена.
Временный глобальный каталог страниц содержится в переменной
swapperpgdir. Временные Таблицы Страниц хранятся в страничных кадрах,
начиная с рдО, сразу после конца неинициализируемых данных (символ end
на рис. 2.13). Для простоты, предположим, что сегменты ядра, временные
Таблицы Страниц и область памяти в 128 Кбайт умещаются в 8 Мбайт опера-
оперативной памяти. Чтобы отобразить 8 Мбайт оперативной памяти, требуются
две Таблицы Страниц.
Цель первого этапа заключается в том, чтобы обеспечить несложную адреса-
адресацию этих 8 Мбайт оперативной памяти как в реальном, так и в защищенном
режиме. Поэтому ядро должно создать отображение двух интервалов линей-
линейных адресов, с 0x00000000 по 0x007fffff и с 0хс0000000 по 0xc07fffff, в ин-
интервал физических адресов с 0x00000000 по 0x007fffff. Иными словами, ядро
на своем первом этапе инициализации может адресовать первые 8 Мбайт
оперативной памяти либо с помощью линейных адресов, идентичных физи-
физическим, либо с помощью 8 Мбайт интервала линейных адресов, начиная с
ОхсООООООО.
Ядро создает необходимое отображение, заполняя нулями все записи пере-
переменной swapperpgdir, кроме записей 0, 1, 0x300 (десятичное 768) и 0x301
(десятичное 769), причем последние две записи охватывают все линейные
адреса с ОхсООООООО по 0xc07fffff. Записи 0, 1, 0x300 и 0x301 инициализиру-
инициализируются следующим образом:
□I в поле адреса записей 0 и 0x300 записывается физический адрес странич-
страничного кадра рдО, а в поле адреса записей 1 и 0x301 — физический адрес
страничного кадра, следующего за рдО;
□ ВО всех четырех Записях устанавливаются флаги Present, Read/Write И
User/Supervisor;
П1 ВО всех четырех Записях сбраСЫВаЮТСЯ флаги Accessed, Dirty, PCD, PWD И
Page Size.
Ассемблерная функция startup_32 о, кроме прочего, включает блок управле-
управления страницами. Это достигается загрузкой физического адреса переменной
swapperpgdir в управляющий регистр сгЗ и установкой флага pg в управ-
управляющем регистре его, как показано в следующем фрагменте кода, эквива-
эквивалентном реальному:
movl $swapper_pg_dir-0xc0000000,%еах
movl %eax,%cr3 /* установить указатель на таблицу страниц.. */
movl %crO,%eax
orl $0x80000000,%еах
movl %eax,%cr0 /* ..и установить бит управления страницами (PG) */
Окончательные Таблицы Страниц ядра
при объеме памяти менее 896 Мбайт
Окончательные отображения адресов, обеспечиваемые таблицами страниц
ядра, должны преобразовывать линейные адреса, начиная с ОхсООООООО, в
физические адреса, начиная с 0.
Макрос ра служит для преобразования линейного адреса, начиная с
pageoffset, в соответствующий физический адрес, а макрос va выполняет
обратное преобразование.
Главный глобальный каталог страниц ядра по-прежнему хранится в перемен-
переменной swapperpgdir. Он инициализируется функцией paginginit (), КОТОрая
выполняет следующие действия:
1. Вызывает функцию pagetabieinit о, чтобы должным образом заполнить
записи Таблицы Страниц.
2. Загружает физический адрес переменной swapperpgdir в управляющий
регистр сгЗ.
3. Если процессор поддерживает механизм РАЕ, и ядро откомпилировано
с поддержкой РАЕ, функция устанавливает флаг рае в управляющем реги-
регистре сг4.
4. Вызывает функцию flushtibaiio, чтобы сделать недействительными
все записи TLB-буфера.
Действия, выполняемые функцией pagetabieinit (), зависят как от объема
имеющейся оперативной памяти, так и от модели процессора. Вначале
рассмотрим простейший случай. Пусть у нашего компьютера меньше
896 Мбайт7 оперативной памяти. Для адресации достаточно 32-битовых фи-
физических адресов, и активизировать механизм расширения физических адре-
адресов необязательно. Глобальный каталог страниц swapperpgdir заново ини-
инициализируется в цикле, эквивалентном следующему:
pgd = swapper_pg_dir + pgd_index(PAGE_OFFSET); /* 7 68 */
phys_addr = 0x00000000;
while (phys_addr < (max_low_pfn * PAGE_SIZE) ) {
pmd = one_md_table_init(pgd); /* возвращает pgd */
set_pmd(pmd, __pmd(phys_addr | pgprot_val( pgprot@xle3))));
/* 0xle3 == Present, Accessed, Dirty, Read/Write,
Page Size, Global */
phys_addr += PTRS_PER_PTE * PAGE_SIZE; /* 0x400000 */
++pgd;
}
Предположим, что на компьютере установлена последняя модель процессора
80x86, поддерживающая 4-мегабайтовые страницы и "глобальные" записи в
TLB-буфере. Обратите внимание, что сброшены флаги user/Supervisor во
всех записях глобального каталога страниц, ссылающихся на линейные адре-
адреса выше ОхсООООООО. Это не позволяет процессам, работающим в пользова-
пользовательском режиме, обращаться к адресному пространству ядра. Обратите вни-
внимание также на то, что флаг Page size установлен, и, следовательно, ядро
может обращаться к оперативной памяти, используя страницы большого раз-
размера.
Тождественное отображение первых мегабайтов физической памяти (в нашем
примере— 8 Мбайт), созданное функцией startup_32 о, необходимо для за-
завершения этапа инициализации ядра. Когда надобность в этом отображении
отпадает, ядро очищает соответствующие записи таблиц страниц с помощью
функции zap_low_mappings ().
На самом деле, это описание не совсем полное. Как мы увидим далее, ядро
также корректирует записи Таблиц Страниц, соответствующие так называе-
называемым фиксированно отображенным линейным адресам.
Окончательные Таблицы Страниц ядра
при объеме памяти от 896 до 4096 Мбайт
В этом случае оперативная память не может быть целиком отображена в про-
пространство линейных адресов ядра. Самое большее, что может сделать Linux
7 Самые верхние 128 Мбайт линейных адресов оставляются доступными для различных отображе-
отображений. Адресное пространство ядра, остающееся для отображения оперативной памяти, составляет
1 Гбайт - 128 Мбайт = 896 Мбайт.
на этапе инициализации, — это отобразить окно оперативной памяти разме-
размером 896 Мбайт в пространство линейных адресов ядра. Если программе по-
понадобится обратиться к другим областям оперативной памяти, нужно будет
отобразить на эти области какой-нибудь другой интервал линейных адресов.
Этот вид динамического отображения обсуждается в главе 8.
Для инициализации глобального каталога страниц ядро выполняет тот же
код, что и в предыдущем случае.
Окончательные Таблицы Страниц ядра
при объеме памяти свыше 4096 Мбайт
Рассмотрим теперь инициализацию Таблицы Страниц ядра на компьютерах с
объемом памяти больше 4 Гбайт. Точнее говоря, мы будем иметь дело со
следующей ситуацией:
□ процессор поддерживает механизм расширения физических адресов
(РАЕ);
□ объем оперативной памяти превышает 4 Гбайт;
□ ядро откомпилировано с поддержкой механизма РАЕ.
Хотя механизм РАЕ работает с 36-битовыми физическими адресами, линей-
линейные адреса по-прежнему имеют длину 32 бита. Как и в предыдущем случае,
Linux отображает окно оперативной памяти размером 896 Мбайт в простран-
пространство линейных адресов ядра. Остальная память будет подвергаться динами-
динамическому отображению, описанному в главе 8. Основное отличие от предыду-
предыдущего случая состоит в применении трехуровневого управления страницами,
так что глобальный каталог страниц инициализируется в цикле, эквивалент-
эквивалентном следующему:
pgd_idx = pgd_index(PAGE_OFFSET); /* 3 */
for (i=0; i<pgd_idx; i++)
set_pgd(swapper_pg_dir + i, pgd( pa(empty_zero_page) + 0x001));
/* 0x001 == Present */
pgd = swapper_pg_dir + pgd_idx;
phys_addr = 0x00000000;
for (; i<PTRS_PER_PGD; ++i, ++pgd) {
pmd = (pmd_t *) alloc_bootmem_low_pages(PAGE_SIZE);
set_pgd(pgd, pgd( pa(pmd) I 0x001)); /* 0x001 == Present */
if (phys_addr < max_low_pfn * PAGE_SIZE)
for (j=0; j < PTRS_PER_PMD /* 512 */
&& phys_addr < max_low_pfn*PAGE_SIZE; ++j) {
set_pmd(pmd, __pmd(phys_addr |
pgprot_val( pgprot@xle3)))) ;
/* 0xle3 == Present, Accessed, Dirty, Read/Write,
Page Size, Global */
phys_addr += PTRS_PER_PTE * PAGE_SIZE; /* 0x200000 */
}
}
swapper_pg_dir[0] = swapper_pg dir[pgd idx] ;
Первые три записи глобального каталога страниц, соответствующие про-
пространству линейных адресов пользователя, инициализируются адресом пус-
пустой страницы (emptyzeropage). Четвертую запись ядро инициализирует ад-
адресом среднего каталога страниц (pmd), выделенного функцией
aiioc_bootmem_iow_pages(). Первые 448 записей среднего каталога страниц
(всего записей 512, по последние 64 зарезервированы для выделения несмеж-
несмежных областей памяти; см. главы 8) заполняются физическими адресами пер-
первых 896 мегабайтов оперативной памяти.
Обратите внимание, что все модели процессоров, которые поддерживают ме-
механизм расширения физических адресов, поддерживают большие страницы в
2 Мбайт и глобальные страницы. Как и в предыдущих случаях, Linux, по
возможности, использует страницы большого размера для сокращения коли-
количества Таблиц Страниц.
После этого четвертая запись глобального каталога страниц копируется в
первую, чтобы отразить отображение нижней физической памяти в первые
896 Мбайт пространства линейных адресов. Это отображение необходимо
для завершения инициализации симметричных многопроцессорных систем
(SMP). Когда надобность в нем отпадает, ядро очищает соответствующие
записи в таблицах страниц с помощью функции zapiowmappings (), как и в
предыдущих случаях.
Фиксированно отображенные линейные адреса
Мы знаем, что начало четвертого гигабайта линейных адресов ядра отобра-
отображает физическую память системы. Однако последние 128 Мбайт адресов все-
всегда остаются доступными, потому что ядро использует их под несмежные
области памяти и фиксированно отображенные линейные адреса.
Выделение несмежных областей памяти — это всего лишь специальный спо-
способ динамического выделения и освобождения памяти. Он описан в главе S, а
в этом разделе мы сосредоточим внимание на фиксированно отображенных
адресах.
По своей сути, фиксированно отображенный линейный адрес — это постоян-
постоянный линейный адрес, например, OxffffcOOO, у которого соответствующий фи-
физический адрес необязательно вычисляется по формуле "линейный адрес ми-
нус ОхсОООООО", а устанавливается произвольно. Таким образом, каждый
фиксированно отображенный линейный адрес отображает один страничный
кадр физической памяти. Как мы увидим в следующих главах, ядро пользует-
пользуется фиксированно отображенными линейными адресами вместо указателей, не
меняющих свое значение.
Фиксированно отображенные линейные адреса концептуально аналогичны
линейным адресам, отображающим первые 896 Мбайт оперативной памяти.
Однако фиксированно отображенный линейный адрес может соответствовать
любому физическому адресу, а отображение, установленное линейными ад-
адресами в начальной части четвертого гигабайта, является линейным (линей-
(линейный адрес X соответствует физическому адресу X-page_offset).
По сравнению с переменными-указателями, фиксированно отображенные
линейные адреса более эффективны. На практике разыменование указателя
требует на одно обращение к памяти больше, чем разыменование непосред-
непосредственного постоянного адреса. Кроме того, проверка значения указателя пе-
перед разыменованием является частью хорошего стиля программирования, а
для постоянного линейного адреса никакая проверка не требуется.
Каждый фиксированно отображенный линейный адрес представлен неболь-
небольшим целочисленным индексом В структуре enum fixed_addresses:
enum fixed_addresses {
FIX_HOLE,
FIX_VSYSCALL,
FIX_APIC_BASE,
FIX_IO_APIC_BASE_0,
end_of_fixed_addresses
};
Фиксированно отображенные линейные адреса помещаются в конец четвер-
четвертого гигабайта линейных адресов. Функция fixtovirto вычисляет посто-
постоянный линейный адрес по индексу:
inline unsigned long fix_to_virt(const unsigned int idx)
{
if (idx >= end_of_fixed_addresses)
this_f ixmap_does_not_exist () ;
return (OxfffffOOOUL - (idx « PAGE_SHIFT));
}
Предположим, некая функция ядра вызывает f ixtovirt (fixioapicbaseo) .
Поскольку функция объявлена с атрибутом inline, компилятор С не генери-
генерирует вызов функции fixtovirt о, а вставляет ее код в вызывающую функ-
цию. Кроме того, проверка значения индекса на этапе выполнения не произ-
производится. На самом деле, значение fixioapicbaseo является константой и
равно 3, так что компилятор может "обрезать" условный оператор, потому
что его условие ложно на этапе компиляции. Если это условие истинно, или
аргумент функции fixtovirt о не является константой, компилятор выдает
ошибку на этапе компоновки, потому что символ
thisfixmapdoesnotexist нигде не определен. В конечном счете, компи-
компилятор вычисляет выражение Oxfffffooo-C«PAGE_SHIFT) и заменяет вызов
функции fixtovirt о на постоянный линейный адрес Oxff ffcooo.
Чтобы связать физический адрес с фиксированно отображенным линейным
адресом, ЯДрО вызывает макрОСЫ set_fixmap(idx,phys) И set_fixmap_
nocache(idx,phys). Оба макроса инициализируют запись Таблицы Страниц,
соответствующую линейному адресу, вычисляемому функцией
fixtovirt(idx) физическим адресом phys. Заметим, что второй макрос,
кроме прочего, устанавливает флаг pcd в записи Таблицы Страниц, тем са-
самым отключая использование аппаратного кэша при обращении к данным в
страничном кадре. Макрос ciearfixmap(idx) разрывает связь между фикси-
фиксированно отображенным линейным адресом idx и физическим адресом.
Работа с аппаратным кэшем и TLB-буфером
Последняя тема, связанная с адресацией памяти, освещает вопросы опти-
оптимального использования аппаратных кэшей ядром. Аппаратные кэши и TLB-
буферы играют важнейшую роль в повышении производительности совре-
современных компьютеров. Для уменьшения количества промахов мимо кэша и
TLB-буфера разработчики ядра применяют ряд приемов.
Работа с аппаратным кэшем
Как было замечено ранее в этой главе, аппаратные кэши адресуются с по-
помощью строк кэшей. Макрос licachebytes возвращает размер строки кэша
в байтах. В процессорах Intel, предшествовавших модели Pentium 4, этот мак-
макрос возвращал 32; для Pentium 4 он возвращает 128.
Чтобы оптимизировать процент попаданий в кэш, ядро принимает во внима-
внимание архитектуру и принимает следующие решения:
□ наиболее часто используемые поля структуры данных располагаются
внутри этой структуры с меньшими смещениями, чтобы они могли быть
кэшированы в одной строке;
□ при выделении большого количества структур ядро старается сохранить
каждую структуру в памяти так, чтобы все строки кэша заполнялись рав-
равномерно.
Синхронизация кэшей выполняется микропроцессорами 80x86 автоматиче-
автоматически, и поэтому для этих процессоров ядро Linux не выполняет сброс аппарат-
аппаратных кэшей. Однако оно все-таки предоставляет интерфейс для сброса кэшей
процессорам, не синхронизирующим кэши.
Работа с TLB-буфером
Процессоры не могут автоматически синхронизировать свои TLB-кэши, по-
потому что ядро, а не аппаратная часть принимает решение о дальнейшей кор-
корректности отображения между линейным адресом и физическим.
В Linux 2.6 имеется несколько методов сброса TLB-буферов, которые следует
применять аккуратно, в зависимости от вида изменения таблицы страниц
(табл. 2.11).
Таблица 2.11. Архитектурно-независимые методы объявления TLB-буферов
недействительными
Имя мето*а °™сание применение
f lushtlball Сбрасывает все записи TLB-буфера Изменение записей
(включая те, что относятся к гло- таблицы страниц ядра
бальным страницам, т. е. страницам
с установленным флагом Global)
flush_tlb_kernel_range Сбрасывает все записи TLB-буфера Изменение отдельного
(включая те, что относятся к гло- диапазона записей
бальным страницам) таблицы страниц ядра
flush_tlb Сбрасывает все записи TLB-буфера Переключение про-
неглобальных страниц, принадле- цессов
жащих текущему процессу
f lush_tlbjnm Сбрасывает все записи TLB-буфера Ответвление нового
неглобальных страниц, принадле- процесса
жащих заданному процессу
f iush_tlb_range Сбрасывает записи TLB-буфера, Освобождение интер-
интерсоответствующие интервалу линей- вала линейных адре-
ных адресов заданного процесса сов процесса
f lush_tlb_pgtables Сбрасывает записи TLB-буфера Освобождение от-
заданного непрерывного подмно- дельных таблиц стра-
жества таблиц страниц заданного ниц процесса
процесса
f lush_tlb_page Сбрасывает TLB-буфер одной запи- Обработка ошибки
си Таблицы Страниц заданного про- обращения к странице
цесса
Несмотря на богатый выбор методов TLB-буфера, предлагаемых типичным
ядром Linux, каждый микропроцессор обычно имеет куда более ограничен-
ный набор ассемблерных инструкций для сброса TLB-буферов. В этом смыс-
смысле одной из самых гибких платформ является UltraSPARC фирмы Sun. В про-
противоположность ей, микропроцессоры Intel предлагают только два способа
объявления содержимого TLB-буферов недействительными:
□ все модели Pentium автоматически сбрасывают записи TLB-буферов, от-
относящиеся к неглобальным страницам, когда в регистр сгз загружается
какое-либо значение;
□ в Pentium Pro и более поздних моделях ассемблерная инструкция invipg
делает недействительной одну запись TLB-буфера, отображающую задан-
заданный линейный адрес.
В табл. 2.12 перечислены макросы операционной системы Linux, которые
эксплуатируют данные аппаратные методы. Эти макросы являются основ-
основными составляющими архитектурно-независимых методов, приведенных в
табл. 2.11.
Таблица 2.12. Макросы сброса TLB-буферов для Pentium Pro
и более поздних моделей
Имя макроса Описание Где используется
f lush_tlb () Записывает содержимое регистра f lush_tlb,
сгЗ обратно в этот регистр f lush_tlb_mm,
flush_tlb_range
f lush_tlb_global () Запрещает использование гло- f lush_tlb_all,f
бальных страниц, сбрасывая lush_tlb_kernel_range
флаг pge в регистре сг4, записы-
записывает содержимое регистра сгЗ
обратно в этот регистр и вновь
устанавливает флаг pge
flush_tlb_single(addr) Выполняет ассемблерную инст- flush_tlb_page
рукцию invipg с параметром
addr
Обратите внимание, что в табл. 2.12 отсутствует метод flushtibpgtabies.
Дело в том, что в архитектуре 80x86 ничего не нужно предпринимать, когда
таблица страниц отсоединяется от таблицы-родителя, и поэтому функция,
реализующая этот метод, пуста.
Архитектурно-независимые методы объявления TLB-буферов недействи-
недействительными довольно просто расширяются на многопроцессорные системы.
Функция, выполняющаяся на некотором процессоре, посылает другим про-
процессорам межпроцессорное прерывание (см. разд. "Обработка межпроцес-
межпроцессорных прерываний" главы 4), которое заставляет их выполнить соответст-
соответствующую функцию сброса TLB-буфера.
Вообще говоря, любое переключение процессов подразумевает смену набора
активных таблиц страниц. Записи локальных TLB-буферов, имеющие отно-
отношение к старым таблицам страниц, должны быть сброшены. Это делается
автоматически, когда ядро пишет адрес нового глобального каталога страниц
в управляющий регистр сгЗ. Однако ядро успешно избегает сброса TLB-
буферов в следующих случаях:
□ при переключении между обычными процессами, использующими один
набор таблиц страниц (см. разд. "Функция scheduleQ главы 7);
П при переключении между обычным процессом и потоком ядра. Как мы
увидим в главе 9, потоки ядра не имеют собственного набора таблиц стра-
страниц. Вместо этого они пользуются таблицами страниц, принадлежащими
обычному процессу, который был последним запланирован на выпол-
выполнение.
Помимо переключения процессов, есть и другие случаи, в которых ядру не-
необходимо сбросить некоторые записи TLB-буфера. Например, когда ядро
присваивает страничный кадр процессу режима пользователя и сохраняет его
физический адрес в записи Таблицы Страниц, оно должно сбросить любую
запись в локальном TLB-буфере, которая ссылается на соответствующий ли-
линейный адрес. В многопроцессорных системах ядро также должно сбрасы-
сбрасывать ту же запись TLB-буферов на других процессорах, использующих тот
же набор таблиц страниц, если таковые имеются.
Чтобы избежать ненужного сбрасывания TLB-буферов в многопроцессорных
системах, ядро прибегает к приему, называемому "ленивым" режимом TLB.
В его основе лежит следующая идея: если несколько процессоров использу-
используют одни и те же таблицы страниц, и некая запись в TLB-буфере должна быть
сброшена у всех процессоров, то сброс в некоторых ситуациях может быть
отложен для процессоров, выполняющих потоки ядра.
Дело в том, что поток ядра не имеет своего набора таблиц страниц. Вместо
этого он пользуется таблицами страниц, принадлежащими обычному процес-
процессу. Однако нет необходимости объявлять недействительной запись TLB-
буфера, которая ссылается на линейный адрес режима пользователя, потому
что никакой поток ядра не обращается к адресному пространству режима
пользователя8.
Когда какой-нибудь процессор начинает выполнять поток ядра, ядро перево-
переводит его в "ленивый" режим TLB. Когда поступают запросы на очистку неко-
некоторых записей TLB-буфера, процессор в "ленивом" режиме TLB не сбрасыва-
сбрасывает эти записи, а запоминает, что его текущий процесс использует набор таб-
8 Между прочим, метод f lush_tlb_all не использует "ленивый" режим TLB. Обычно он вызыва-
вызывается, когда ядро модифицирует запись Таблицы Страниц, относящуюся к адресному пространству
режима ядра.
лиц страниц, у которых записи TLB-буфера для адресов режима пользователя
некорректны. Как только процессор в "ленивом" режиме TLB переключается
на обычный процесс с другим набором таблиц страниц, аппаратная часть ав-
автоматически сбрасывает записи TLB-буфера, а ядро выводит процессор из
"ленивого" режима TLB. Если же процессор в "ленивом" режиме TLB пере-
переключается на обычный процесс с тем же набором таблиц страниц, что у по-
потока ядра, работавшего до него, то отложенный сброс TLB-буфера должен
быть выполнен ядром. Такой "запоздалый" сброс достигается сбросом всех
неглобальных записей TLB-буфера данного процессора.
Для реализации "ленивого" режима TLB требуются дополнительные структу-
структуры данных. Переменная cputibstate представляет собой статический массив
из nrcpus структур (значением этого макроса по умолчанию является 32; он
определяет максимальное количество процессоров в системе), состоящих из
поля activemm, указывающего на дескриптор памяти текущего процесса
(см. главу 9), и из флага state, принимающего значение tlbstateok (не "ле-
"ленивый" режим TLB) или tlbstatelazy ("ленивый" режим TLB). Кроме того,
каждый дескриптор памяти включает в себя поле cpuvmmask, хранящее ин-
индексы процессоров, которым следует принять межпроцессорные прерывания,
относящиеся к сбросу TLB-буферов. Это поле имеет осмысленное значение,
только когда дескриптор памяти принадлежит процессу, выполняемому в
данный момент.
Когда процессор начинает выполнять поток ядра, ядро записывает в поле
state его элемента cpu_tibstate значение tlbstate_lazy. Кроме того, поле
cpuvmmask активного дескриптора памяти хранит индексы всех процессоров
в системе, включая индекс того, который перешел в "ленивый" режим TLB.
Если другой процессор захочет объявить недействительными записи TLB-
буферов у всех процессоров, работающих с данным набором таблиц страниц,
он доставит межпроцессорное прерывание всем процессорам, индексы кото-
которых содержатся в поле cpuvmmask соответствующего дескриптора памяти.
Когда процессор получает межпроцессорное прерывание, имеющее отноше-
отношение к сбросу TLB-буфера, и убеждается, что оно затрагивает набор таблиц
страниц его текущего процесса, он проверяет поле state элемента
cpu tibstate на равенство значению tlbstatelazy. В этом случае ядро отка-
отказывается сбросить записи TLB-буфера и удаляет индекс процессора из поля
cpuvmmask дескриптора процесса. Последствия этик действий таковы:
□ пока процессор остается в "ленивом" режиме TLB, он не получает другие
межпроцессорные прерывания, относящиеся к сбросу TLB-буфера;
□ если процессор переключается на другой процесс, который пользуется тем
же набором таблиц страниц, что и замещаемый поток ядра, то ядро вызы-
вызывает функцию flushtibo, чтобы сбросить неглобальные записи TLB-
буфера этого процессора.
ГЛАВА 3
Процессы
Понятие процесса является фундаментальным в любой многозадачной опера-
операционной системе. Процесс обычно определяется как выполняемый экземпляр
программы. Так, если 16 пользователей запустят программу vi, в системе бу-
будет работать 16 отдельных процессов (хотя все они используют один и тот же
код). В исходном коде Linux процессы часто называются задачами или пото-
потоками.
В этой главе мы обсудим статические свойства процесса, а затем опишем, как
в ядре происходит переключение процессов. В последних двух разделах опи-
описано, как можно создавать и уничтожать процессы. Мы также покажем, как в
Linux организована поддержка многопоточных приложений: как было сказа-
сказано в главе 7, при этом Linux использует так называемые облегченные про-
процессы.
Процессы, облегченные процессы и потоки
Термин "процесс" имеет несколько различных значений. В этой книге мы бу-
будем придерживаться определения, которое обычно дается в учебниках по
операционным системам: процесс — это выполняемый экземпляр програм-
программы. Можно считать процесс неким множеством структур данных, которое
полностью описывает, как далеко продвинулось выполнение программы.
Процессы в чем-то похожи на людей: они рождаются, живут более-менее
долго, могут породить одного или нескольких потомков и, в конце концов,
умирают. Небольшое отличие состоит в том, что секс — не слишком распро-
распространенное явление в среде процессов: любой процесс имеет только одного
родителя.
С точки зрения ядра, предназначение процесса в том, чтобы действовать как
отдельная сущность, которой выделены системные ресурсы (процессорное
время, память и т. д.).
Когда процесс создается, он почти идентичен своему родителю. Он получает
(логическую) копию адресного пространства родителя и выполняет тот же
код, что и родитель, начиная с инструкции, следующей за системным вызо-
вызовом, породившим процесс. Хотя родитель и потомок могут совместно ис-
использовать страницы, содержащие код (текстовые секции) программы, они
обладают собственными копиями данных (стека и кучи), так что изменения,
сделанные потомком по какому-либо адресу памяти, не видны родителю
(и наоборот).
Ранние версии ядер Unix придерживались этой простой модели, но в совре-
современных Unix-системах она больше не используется. В настоящее время под-
поддерживаются многопоточные приложения— программы, имеющие много
относительно независимых ветвей, совместно использующих значительную
часть структур данных, принадлежащих приложению. В таких системах про-
процесс состоит из нескольких пользовательских потоков (или просто потоков),
каждый из которых представляет выполняемую ветвь процесса. Сейчас мно-
многопоточные приложения пишутся с помощью стандартных наборов библио-
библиотечных функций, называемых библиотеками pthread (то есть библиотеками
POSIX-потоков).
Старые версии ядра Linux не предлагали разработчикам поддержку многопо-
многопоточных приложений. С точки зрения ядра, многопоточное приложение было
обычным процессом. Ветви выполнения многопоточного приложения созда-
создавались, обрабатывались и планировались целиком в режиме пользователя,
как правило, с помощью POSIX-совместимой библиотеки pthread.
Однако такую реализацию многопоточных приложений нельзя признать
удачной. Представим, например, программу, играющую в шахматы, которая
имеет два потока: один управляет графической шахматной доской, ждет, ко-
когда шахматист сделает ход, и отображает ходы компьютера, а другой — рас-
рассчитывает следующий ход. Пока первый поток ждет хода противника, второй
должен непрерывно работать, используя то время, в которое шахматист об-
обдумывает свой ход. Если такая программа выполняется как один процесс,
первый поток не может просто сделать блокирующий системный вызов в
ожидании действий пользователя, ведь в таком случае второй поток также
окажется заблокированным. Вместо этого первый поток должен прибегать к
сложным неблокирующим методикам, чтобы обеспечить непрерывное вы-
выполнение процесса.
Для оптимизации поддержки многопоточных приложений в Linux использу-
используются облегченные процессы. Идея состоит в том, что два облегченных про-
цесса могут совместно использовать определенные ресурсы, такие как адрес-
адресное пространство, открытые файлы и т. д. Когда один из них модифицирует
совместно используемый ресурс, другой немедленно видит изменения. Ко-
Конечно, эти два процесса должны синхронизировать свой доступ к общему
ресурсу.
Простейший способ реализовать многопоточное приложение заключается в
том, чтобы ассоциировать с каждым потоком облегченный процесс. Тогда
потоки смогут обращаться к набору структур данных приложения, используя
общее адресное пространство, один набор открытых файлов и т. д. В то же
время ядро может распланировать независимое выполнение каждого потока
так, что один будет спать, а другой— работать. Примерами POSIX-
совместимых библиотек pthread, использующих облегченные потоки Linux,
являются LinuxThreads, Native POSIX Thread Library (NPTL) и Next
Generation Posix Threading Package (NGPT) от IBM.
POSIX-совместимые многопоточные приложения лучше всего работают с
ядрами, поддерживающими "группы потоков". В Linux группой потоков на-
называется набор облегченных процессов, которые реализуют многопоточное
приложение и действуют как единое целое по отношению к некоторым сис-
системным вызовам, таким как getpid (), kill () и exit (). Мы подробно опишем
их далее в этой главе.
Дескриптор процесса
Чтобы управлять процессами, ядро должно иметь картину того, что делает
каждый процесс. Оно должно знать, например, приоритет процесса, выпол-
выполняется ли он процессором или блокирован в ожидании какого-то события,
какое ему присвоено адресное пространство, к каким файлам ему разрешено
обращаться и т. д. Для этой цели служит дескриптор процесса, структура ти-
типа taskstruct, поля которой содержит всю информацию, касающуюся одно-
одного процесса1. Будучи хранилищем такого объема информации, дескриптор
процесса является довольно сложной структурой. Помимо большого количе-
количества полей, содержащих атрибуты процесса, его дескриптор включает в себя
несколько указателей на другие структуры, которые, в свою очередь, содер-
содержат указатели на третьи структуры. На рис. 3.1 схематически изображен де-
дескриптор процесса в Linux.
Шесть структур данных справа на рисунке обозначают специфические ресур-
ресурсы, принадлежащие процессу. Большинство из них будет описано в следую-
1 В ядре также определен тип task_t, эквивалентный struct task_struct.
щих главах. В этой главе мы сосредоточим внимание на полях двух типов,
которые описывают состояние процесса и отношения родитель-потомок.
Рис. 3.1. Дескриптор процесса в Linux
Состояние процесса
Как можно догадаться по названию поля state дескриптора процесса, оно
показывает, что происходит с процессом в данный момент. Оно состоит из
массива флагов, каждый из которых обозначает возможное состояние про-
процесса. В текущих версиях Linux эти состояния взаимно исключают друг дру-
друга, и, следовательно, в поле state всегда установлен только один флаг, а ос-
остальные сброшены. Перечислим возможные состояния процесса:
□ taskjrunning — процесс либо выполняется процессором, либо ждет своего
выполнения;
□ taskinterruptible — процесс приостановлен ("спит") до тех пор, пока не
будет удовлетворено некоторое условие. Возникновение аппаратного пре-
прерывания, освобождение системного ресурса, ожидаемого процессом, или
доставка процессу сигнала являются примерами условий, способных раз-
разбудить процесс (перевести его обратно в состояние taskrunning);
□ TASK_UNINTERRUPTIBLE анаЛОГИЧНО СОСТОЯНИЮ TASKJENTERRUPTIBLE С Тем
отличием, что доставка сигнала спящему процессу не изменяет его со-
состояние. Это состояние используется редко. Оно представляет ценность в
некоторых специфических ситуациях, когда процесс должен ждать насту-
наступления некоторого события, причем прерывать этот процесс нельзя. На-
Например, этим состоянием можно воспользоваться, когда процесс открыва-
открывает файл устройства, и соответствующий драйвер начинает опрашивать ап-
аппаратное устройство. Этот драйвер нельзя прерывать, пока не закончится
опрос, в противном случае аппаратное устройство останется в непредска-
непредсказуемом состоянии;
□ taskstopped— выполнение процесса остановлено. Процесс переходит в
это состояние, когда получает сигнал sigstop, sigtstp, sigttin и sigttou;
□ tasktraced— выполнение процесса остановлено отладчиком. Когда ве-
ведется мониторинг процесса со стороны другого процесса (например, от-
отладчик делает системный вызов ptrace (), чтобы выполнить мониторинг
тестируемой программы), любой сигнал может перевести процесс в со-
состояние TASK_TRACED.
В поле state, а также в поле exitstate дескриптора процесса можно хранить
два дополнительных состояния. Как следует из названия второго поля, про-
процесс переходит в одно из этих состояний, когда его выполнение заканчива-
заканчивается:
□ exitzombie — выполнение процесса завершено, но процесс-родитель еще
не сделал системный вызов wait4 о или waitpidO, возвращающий инфор-
информацию о "скончавшемся" процессе2. До того, как будет сделан системный
вызов из группы wait, ядро не может уничтожить данные в дескрипторе
завершившегося процесса, потому что они могут понадобиться родителю;
□ exitdead— заключительное состояние процесса. Он уничтожается сис-
системой, потому что процесс-родитель только что сделал для него систем-
системный вызов wait4() или waitpidO. Переход из состояния exit_zombie в
exitdead позволяет избежать конфликтов одновременного обращения,
2 Существуют и другие библиотечные функции, принадлежащие группе wait, например, wait3 ()
и wait (), однако в Linux они реализованы системными вызовами wait4 () и waitpid ().
если другие потоки сделают системный вызов из группы wait для того же
процесса (см. главу 5).
Значение поля state обычно устанавливается обычным присваиванием, на-
например:
p->state = TASK_RUNNING;
Ядро также ИСПОЛЬЗует макросы set_task_state И setcurrentstate. Они За-
дают состояние указанного процесса и текущего процесса соответственно.
Кроме того, эти макросы гарантируют, что операция присваивания не будет
смешана с другими инструкциями компилятором или управляющим блоком
процессора. Изменение порядка инструкций иногда приводит к катастрофи-
катастрофическим результатам (см. главу 5).
Идентификация процесса
В качестве общего правила можно утверждать, что каждый контекст выпол-
выполнения, который может быть запланирован к выполнению независимо, должен
иметь собственный дескриптор процесса. Поэтому даже облегченные процес-
процессы, которые совместно используют значительную часть структур данных,
обладают своими структурами типа taskstruct.
Строго однозначное соответствие между процессом и его дескриптором де-
делает 32-битовый адрес3 структуры taskstruct удобным для ядра средством
идентификации процесса. Такие адреса называются указателями на дескрип-
дескрипторы процессов. Большинство ссылок на процессы в ядре делается с по-
помощью указателей на дескрипторы.
С другой стороны, Unix-подобные операционные системы позволяют пользо-
пользователям идентифицировать процессы с помощью числа, называемого иден-
идентификатором процесса (PID, Process ID), которое хранится в поле pid деск-
дескриптора процесса. Идентификаторы процессов нумеруются последовательно:
идентификатор нового процесса обычно на единицу больше идентификатора
процесса, созданного перед этим. Конечно, существует верхний предел для
значений идентификаторов. Когда ядро его достигает, оно начинает исполь-
использовать меньшие свободные идентификаторы. По умолчанию максимальный
идентификатор процесса равен 32 767 (pidmaxdefault - l), но системный
администратор может понизить предел, записав меньшее значение в файл
/proc/sys/kernel/pidmax (/proc — это точка монтирования специальной
файловой системы, см. разд. "Специальные файловые системы" главы 12).
3 Хотя технически эти 32 бита являются лишь составляющей логического адреса, определяющей
смещение, они совпадают с линейным адресом.
В 64-разрядных архитектурах системный администратор может увеличить
максимальный идентификатор процесса до 4 194 303.
При повторном использовании освободившихся идентификаторов процессов
ядро поддерживает битовую карту pidmaparray, которая показывает, какие
идентификаторы в данный момент заняты, а какие — нет. Поскольку стра-
страничный кадр содержит 32 768 битов, в 32-разрядных архитектурах битовая
карта pidmaparray занимает одну страницу. В 64-разрядных архитектурах к
битовой карте могут быть добавлены дополнительные страницы, если ядро
присвоит процессу идентификатор, слишком большой для текущего размера
битовой карты. Эти страницы уже не освобождаются.
Linux ассоциирует индивидуальный идентификатор с каждым процессом и
облегченным процессом в системе. (Как мы увидим чуть позже в этой главе,
в многопроцессорных системах существует маленькое исключение из этого
правила.) Такой подход обеспечивает максимальную гибкость, потому что
позволяет уникальным образом идентифицировать каждый контекст выпол-
выполнения в системе.
С другой стороны, программисты в Unix хотели бы, чтобы у потоков в одной
группе был общий идентификатор. Например, им нужно иметь возможность
посылать сигнал с указанием идентификатора процесса так, чтобы сигнал
повлиял на все потоки в группе. Более того, стандарт POSIX 1003.1с требует,
чтобы все потоки многопоточного приложения имели один идентификатор
процесса.
Чтобы соответствовать этому стандарту, Linux работает с группами потоков.
Идентификатор, общий для всех потоков — это идентификатор лидера груп-
группы потоков (то есть первого облегченного процесса в группе), хранящийся в
поле tgid дескрипторов процесса. Системный вызов getpid () возвращает для
текущего процесса значение tgid, а не pid, так что у всех потоков многопо-
многопоточного приложения будет один идентификатор. Большинство процессов
принадлежит к группе потоков, состоящей из единственного члена. Являясь
лидерами своих групп, они имеют одинаковые значения в полях tgid и pid, и
для них системный вызов getpid () работает как обычно.
Далее в этой главе мы продемонстрируем, как можно эффективно получить
указатель на дескриптор процесса. Эффективность здесь важна, потому что
во многих системных вызовах, например, kill о, для задания процесса ис-
используется его идентификатор.
Работа с дескрипторами процессов
Процессы являются динамическими сущностями, и время их жизни может
продолжаться от нескольких миллисекунд до нескольких месяцев. Следова-
Следовательно, ядро должно уметь работать с несколькими процессами одновремен-
но, и дескрипторы процессов хранятся в динамической памяти, а не в облас-
области, постоянно выделенной ядру. Для каждого процесса Linux помещает две
разные структуры данных в одну область памяти, свою у каждого процесса.
Это небольшая структура, связанная с дескриптором процесса (структура
threadinf о), и стек режима ядра для данного процесса. Размер этой области
обычно равен 8192 байтам (двум страничным кадрам). По соображениям эф-
эффективности, ядро хранит 8-килобайтовую область памяти в двух соседних
страничных кадрах, причем первый из них выровнен по границе 213. Это мо-
может обернуться проблемой, если доступно мало динамической памяти, пото-
потому что свободная память может оказаться сильно фрагментированной (см.
главу 8). Поэтому в архитектуре 80x86 можно сконфигурировать ядро на эта-
этапе компиляции так, чтобы область памяти, включающая в себя стек и струк-
структуру threadinf о, занимала один страничный кадр D096 байтов).
В главе 2 говорилось, что процесс в режиме ядра обращается к стеку, содер-
содержащемуся в сегменте данных ядра, и это не тот стек, с которым процесс ра-
работает в режиме пользователя. Поскольку управляющие тракты ядра мало
пользуются стеком, требуется лишь несколько тысяч байтов, и восьми кило-
килобайтов вполне достаточно для стека и структуры threadinfo. Однако когда
стек и структура threadinf о содержатся в одном страничном кадре, ядро ис-
использует несколько дополнительных стеков, чтобы избежать переполнения
при большой вложенности прерываний и исключений (см. главу 4).
На рис. 3.2 изображено, как эти две структуры данных располагаются в двух-
двухстраничной (8 Кбайт) области памяти. Структура threadinfo находится в
начале области памяти, а стек растет вниз, начиная с конца этой области. На
рисунке также показано, что между структурами threadinfo и taskstruct
установлена Взаимная СВЯЗЬ При ПОМОЩИ ПОЛеЙ task И threadinf о.
Регистр esp является указателем на стек процессора. Он служит для адреса-
адресации верхушки стека. В системах с архитектурой 80x86 стек начинается в
конце области памяти и растет в направлении ее начала. Сразу после пере-
переключения из режима пользователя в режим ядра стек процесса в этом режи-
режиме пуст, и, следовательно, регистр esp указывает на байт, непосредственно
следующий за стеком.
Значение регистра esp уменьшается, как только в стек записываются данные.
Поскольку структура threadinfo занимает 52 байта, стек ядра может разрас-
разрастаться до 8140 байтов.
Язык С позволяет представить структуру threadinf о и стек ядра, используе-
используемый процессом, в виде следующего объединения:
union thread_union {
struct thread_info thread_info;
unsigned long stack[2048]; /* 1024 для стеков в 4 Кбайт */
};
Рис. 3.2. Структура thread_inf о и стек процесса в режиме ядра,
расположенные в двух страничных кадрах
Структура threadinfo, изображенная на рис. 3.2, хранится, начиная с адреса
0x015fa000, а стек — с адреса 0x015fc000. Регистр esp указывает на верхушку
стека по адресу 0x015fa878.
Ядро вызывает макросы alloc_thread_info И freethreadinfo, КОГДа ему
нужно выделить и, соответственно, освободить область памяти, содержащую
структуру threadinf о и стек ядра.
Идентификация текущего процесса
Тесная связь между структурой threadinf о и стеком режима ядра, описанная
в предыдущем разделе, предлагает эффективное решение: ядро может легко
вычислить адрес структуры threadinfo текущего процесса по значению ре-
регистра esp. В самом деле, если структура threadunion имеет длину 8 Кбайт
B13 байт), то ядро маскирует 13 младших битов регистра esp, чтобы получить
базовый адрес структуры threadinfo. С другой стороны, если структура
threadunion имеет длину 4 Кбайт, ядро маскирует 12 младших битов регист-
регистра esp. Это делается с помощью функции currentthreadinf о (), которая вы-
выполняет примерно следующие ассемблерные инструкции:
movl $0xffffe000,%ecx /* или OxfffffOOO для стеков в 4 Кбайт */
andl %esp,%ecx
movl %ecx,p
После выполнения этих трех инструкций переменная р содержит указатель на
структуру threadinf о процесса, работающего на процессоре, выполнившем
инструкцию.
Чаще всего ядру нужно обратиться к дескриптору процесса, а не к структуре
threadinfo. Чтобы получить указатель на дескриптор текущего процесса,
ядро вызывает макрос current, который фактически эквивалентен current_
thread_info()->task и выполняет примерно следующие ассемблерные инст-
инструкции:
movl $0xffffe000,%ecx /* или OxfffffOOO для стеков в 4 Кбайт */
andl %esp,%ecx
movl (%ecx),p
Поскольку поле task имеет в структуре threadinfo смещение 0, после вы-
выполнения этих трех инструкций переменная р содержит указатель на деск-
дескриптор текущего процесса.
Макрос current часто появляется в коде ядра в качестве префикса к полям
дескриптора процесса. Например, конструкция current->pid возвращает
идентификатор текущего процесса.
Еще одно достоинство совместного хранения дескриптора процесса и стека
проявляется в многопроцессорных системах. Текущий процесс для каждого
процессора определяется простой операцией со стеком, как показано ранее.
В ранних версиях Linux стек ядра и дескриптор процесса хранились порознь.
Тогда приходилось пользоваться глобальной статической переменной
current, чтобы идентифицировать дескриптор работающего процесса. В мно-
многопроцессорных системах переменная current была массивом, содержащим
по элементу на каждый процессор.
Двунаправленные списки
Прежде чем идти дальше и описывать, как ядро отслеживает различные про-
процессы в системе, мы хотели бы подчеркнуть роль специальных структур, реа-
реализующих двунаправленные списки.
Для каждого списка должен быть реализован набор базовых операций: ини-
инициализация списка, вставка и удаление элемента, перебор элементов списка.
Дублирование этих операций для каждого конкретного списка было бы не-
непроизводительной тратой рабочего времени программистов и столь же не-
непроизводительной тратой памяти.
Поэтому в Linux определена структура listhead, два поля которой, next и
prev, содержат указатели вперед и назад для элемента двунаправленного спи-
списка общего назначения. При этом важно отметить, что указатели в поле
listhead содержат адреса других полей listhead, а не адреса структур,
включающих в себя структуру listhead (рис. 3.3, а).
НОВЫЙ СПИСОК СОЗДаеТСЯ С ПОМОЩЬЮ Макроса LIST_HEAD(list_name). Он объ-
являет новую переменную с именем listname, имеющую тип listhead, ко-
торая представляет собой пустой первый элемент, резервирующий место для
головы нового списка. Кроме того, макрос инициализирует поля prev и next
структуры listhead так, что переменная listname указывает сама на себя
(рис. 3.3, б).
Рис. 3.3. Двунаправленные списки, построенные из структур list_head
Базовые операции над списками реализованы несколькими функциями и
макросами, которые перечислены в табл. 3.1.
Таблица 3.1. Функции и макросы для работы со списками
Имя Описание
list_add(n,p) Вставляет элемент, на который указывает п, сразу
после элемента, на который указывает р. Чтобы вста-
вставить элемент п в начало списка, передайте адрес
головы списка в качестве аргумента р
list_add_tail(n,p) Вставляет элемент, на который указывает п, непо-
непосредственно перед элементом, на который указыва-
указывает р. Чтобы вставить элемент п в конец списка, пере-
передайте адрес головы списка в качестве аргумента р
list_del (p) Удаляет элемент, на который указывает р. Задавать
голову списка нет необходимости
list_empty(p) Проверяет, пуст ли список, задаваемый адресом р
головы списка
Таблица 3.1 (окончание)
Имя Описание
list_entry(p,t,m) Возвращает адрес структуры типа t, включающей в
себя поле типа list_head с именем m и адресом р
list_for_each(p,h) Перебирает элементы списка, заданного адресом h
его головы. После каждой итерации р содержит ука-
указатель на структуру list_head в очередном элементе
list_for_each_entry(p,h,m) Аналогично list_for_each, но здесь возвращается
адрес структуры, включающей в себя структуру
list_head, а не адрес самой структуры list_head
В ядре Linux 2.6 применяется еще один вид двунаправленных списков. Они
отличаются от списков listhead тем, что не являются циклическими. В ос-
основном, они используются в качестве хеш-таблиц, у которых размер имеет
большое значение, а возможности найти последний элемент за постоянное
время нет. Голова такого списка хранится в структуре hiisthead, которая
всего лишь указывает на первый элемент списка (или содержит null, если
список пуст). Каждый элемент представлен структурой hiistnode, которая
содержит указатель next на следующий элемент и указатель pprev на преды-
предыдущий элемент. Поскольку список не циклический, поле pprev первого эле-
элемента и поле next последнего содержит null. Со списком можно работать с
помощью нескольких вспомогательных функций и макросов, аналогичных
Тем, ЧТО приведены В табл. 3.1: hlist_add_head(), hlist_del(), hlist_empty(),
hlist entry, hlist_for each entry И Т. Д.
Список процессов
Первым примером двунаправленного списка, который мы рассмотрим, явля-
является список процессов, в котором собраны вместе все дескрипторы процес-
процессов. Каждая структура taskstruct включает в себя поле tasks типа
listhead, чьи поля prev и next указывают, соответственно, на предыдущий и
следующий элемент taskstruct.
Голова списка процессов представляет собой дескриптор inittask типа
taskstruct. Это дескриптор так называемого процесса0, или процесса
swapper. Поле tasks->prev дескриптора inittask указывает на поле tasks
процесса, занесенного в список последним.
Макросы setlinks и removelinks служат для занесения дескриптора в спи-
список процессов и, соответственно, удаления его из списка. Эти макросы также
определяют родительские отношения процесса.
Еще одним полезным макросом является for_each_process. Он перебирает все
элементы списка процессов и определяется следующим образом:
#define for_each_process(p) \
for (p=&init_task; (p=list_entry((р)->tasks.next, \
struct task_struct, tasks) \
) != &init_task; )
Этот макрос представляет собой заголовок оператора цикла, после которого
программист ставит тело цикла. Обратите внимание, что дескриптор
inittask играет всего лишь роль головы цикла. Макрос начинает работу с
того, что переходит от inittask в следующей задаче и продолжает в том же
духе, пока снова не дойдет до дескриптора inittask (благодаря циклической
структуре списка). На каждой итерации переменная, переданная макросу в
качестве аргумента, содержит адрес очередного дескриптора процесса, воз-
возвращаемого макросом listentry.
Списки процессов в состоянии TASK__RUNNING
При поиске процесса, который должен быть выполнен процессором, ядро
должно рассматривать только выполняемые процессы (то есть те, что нахо-
находятся в состоянии task_running).
В ранних версиях Linux все такие процессы заносились в один список, назы-
называемый очередью на выполнение. Поскольку сопровождать список, упорядо-
упорядоченный по приоритетам процессов, довольно накладно, старые планировщи-
планировщики были вынуждены просматривать весь список, чтобы выбрать "самый дос-
достойный" выполняемый процесс.
В Linux 2.6 очередь на выполнение реализована иначе. Цель этой реализа-
реализации — сделать так, чтобы планировщик выбирал процесс за постоянное вре-
время, независимо от количества выполняемых процессов. Подробное описание
этого нового типа очереди на выполнение мы отложим до главы 7, а сейчас
дадим лишь базовые сведения.
Прием, с помощью которого достигается ускорение работы планировщика,
состоит в разбиении очереди на несколько списков выполняемых процессов,
по одному списку для каждого значения приоритета. Каждый дескриптор
taskstruct имеет поле runiist типа listhead. Если приоритет процесса
равен к (числу от 0 до 139), поле runiist связывает дескриптор процесса со
списком выполняемых процессов, имеющих приоритет к. Кроме того, в мно-
многопроцессорной системе у каждого процессора есть собственная очередь на
выполнение, т. е. собственное множество списков процессов. Это классиче-
классический пример усложнения структур ради повышения производительности.
Чтобы оптимизировать работу планировщика, один список разбивается на
140 списков!
Как мы увидим далее, ядро должно поддерживать большое количество дан-
данных в каждой очереди на выполнение. Основными структурами в такой оче-
очереди являются принадлежащие ей списки дескрипторов процессов. Все эти
списки реализуются одной структурой prioarrayt, поля которой перечис-
перечислены в табл. 3.2.
Таблица 3.2. Поля структуры prioarrayt
Тип Поле Описание
int nr_active Количество дескрипторов процессов
в списках
unsigned long [5] bitmap Битовая карта приоритетов: каждый флаг
установлен тогда и только тогда, когда
соответствующий приоритетный список
не пуст
struct list_head [140] queue 140 голов приоритетных списков
Функция enqueue_task(p, array) заносит дескриптор процесса в список очере-
ди на выполнение. Ее код эквивалентен следующему:
list_add_tail(&p->run_list, &array->queue[p->prio]);
set_bit(p->prio, array->bitmap);
array->nr_active++;
p->array = array;
Поле prio дескриптора процесса хранит динамический приоритет процесса, а
поле array является указателем на структуру prioarrayt текущей очереди
на выполнение. Аналогичным образом, функция dequeue__task(p, array) уда-
удаляет дескриптор процесса из списка.
Взаимоотношения между процессами
Процессы, создаваемые программой, находятся в отношениях "родитель-
потомок". Когда процесс создает несколько потомков, между ними возника-
возникают отношения "братства". Несколько полей в дескрипторе процесса отража-
отражают эти взаимоотношения. Они перечислены в табл. 3.3 для некоторого про-
процесса Р. Процессы 0 и 1 создаются ядром. Как мы увидим далее в этой главе,
процесс 1 (называемый init) является предком всех остальных процессов.
На рис. 3.4 изображены отношения внутри группы процессов. Процесс Р0
последовательно создал процессы PI, P2 и РЗ. Процесс РЗ, в свою очередь,
создал процесс Р4.
Таблица 3.3. Поля дескриптора процесса, отражающие взаимоотношения
между процессами
Имя поля Описание
real_parent Указывает на дескриптор процесса, создавшего процесс Р или на деск-
дескриптор процесса 1 (init), если родительский процесс больше не существу-
существует. Следовательно, когда пользователь запускает фоновый процесс и
выходит из оболочки, фоновый процесс становится потомком процесса
init
parent Указывает на текущего родителя процесса Р (это процесс, которому по-
посылается сигнал, когда процесс-потомок завершает работу). Значение
этого поля обычно совпадает со значением поля real_parent. Оно в не-
некоторых случаях может быть другим, например, когда другой процесс
делает системный вызов ptrace (), запрашивая разрешение на монито-
мониторинг процесса Р (см. разд. "Отслеживание выполнения" главы 20)
children Голова списка всех потомков процесса Р
sibling Указатели на следующий и предыдущий элементы в списке процессов-
"братьев", т. е. имеющих того же родителя, что и процесс Р
Помимо указанных существуют и другие отношения между процессами.
Процесс может быть лидером в группе или в сеансе, он может быть лидером
в группе потоков, а также он может отслеживать выполнение других процес-
процессов (см. разд. "Отслеживание выполнения" главы 20). В табл. 3.4 перечисле-
перечислены поля дескриптора процесса, которые определяют отношения между про-
процессом Р и остальными процессами.
Таблица 3.4. Поля дескриптора процесса, определяющие
"неродительские" отношения
Имя поля Описание
groupleader Указатель на дескриптор процесса-лидера группы процесса Р
signal->pgrp Идентификатор процесса-лидера группы процесса Р
tgid Идентификатор процесса-лидера группы потоков, включающий
процесс Р
signal->session Идентификатор процесса-лидера сеанса процесса Р
ptracechiidren Голова списка всех потомков процесса Р, отслеживаемых отлад-
отладчиком
ptracelist Указатели на следующий и предыдущий элементы в списке от-
отслеживаемых процессов, который принадлежит реальному роди-
родителю процесса Р (используется, когда отслеживается процесс Р)
Рис. 3.4. Отношения "родитель-потомок" в группе из пяти процессов
Таблица pidhash и цепные списки
При некоторых обстоятельствах ядро должно уметь вычислять указатель на
дескриптор процесса по идентификатору процесса. Это бывает, например,
при обслуживании системного вызова kill о. Когда процесс Р1 желает от-
отправить сигнал процессу Р2, он делает системный вызов kill о, задавая в ка-
качестве параметра идентификатор процесса Р2. Ядро получает указатель на
дескриптор процесса по его идентификатору, а затем извлекает из дескрипто-
дескриптора процесса Р2 указатель на структуру, регистрирующую сигналы, ожидаю-
ожидающие доставки.
Последовательный перебор списка процессов и проверка полей pid дескрип-
дескрипторов процессов — допустимое, но неэффективное решение. Для ускорения
поиска были введены четыре хеш-таблицы. Зачем понадобилось несколько
хеш-таблиц? Дело в том, что дескриптор процесса включает в себя поля,
представляющие разные типы идентификаторов процессов (табл. 3.5), и каж-
каждому типу требуется своя хеш-таблица.
Таблица 3.5. Четыре хеш-таблицы и соответствующие поля дескриптора процесса
Тип хеш-таблицы Имя поля Описание
pidtype_pid pid Идентификатор процесса
pidtype_tgid tgid Идентификатор процесса-лидера группы потоков
pidtype_pgid pgrp Идентификатор процесса-лидера группы
pidtype_sid session Идентификатор процесса-лидера сеанса
Эти четыре хеш-таблицы выделяются динамически на этапе инициализации
ядра, и их адреса хранятся в массиве pidhash. Размер одной хеш-таблицы
зависит от объема доступной оперативной памяти. Например, для систем,
имеющих 512 Мбайт памяти, каждая хеш-таблица хранится в четырех стра-
страничных кадрах и содержит 2048 записей.
Идентификатор процесса преобразуется в индекс таблицы с помощью макро-
макроса pidhashf п, который развертывается в следующий код:
#define pid_hashfn(x) hash_long((unsigned long) x, pidhash_shift)
Переменная pidhashshift хранит длину индекса таблицы в битах (в нашем
примере 11). Функция hashiongo используется многими хеш-функциями, и
для 32-разрядных архитектур она эквивалентна следующему коду:
unsigned long hash_long(unsigned long val, unsigned int bits)
{
unsigned long hash = val * 0x9e370001UL;
return hash » C2 - bits);
}
Поскольку в нашем примере значение pidhashshift равно 11, макрос
pidhashfn возвращает значение от 0 до 211 - 1 = 2047.
Магическая константа
Возможно, вы желаете знать, откуда взялась константа 0х9еЗ 70001
(= 2 654 404 609). Данная хеш-функция основана на умножении индекса на
подходящее большое число с такой целью, чтобы результат вызвал перепол-
переполнение, и значение 32-битовой переменной можно было считать результатом
операции деления с остатком. Кнут предположил, что хорошие результаты
можно получить, когда большой сомножитель является простым числом,
примерно равным золотому сечению от 232 C2 бита — это размер регистров в
архитектуре 80x86). Число 2 654 404 609 является простым, близким к
232x(V5 — 1)/2. Кроме того, на него легко умножать, выполняя сложение и
сдвиг битов, потому что оно равно 231 + 229 - 225 + 222 - 219 - 216 + 1.
Как говорится в любом начальном курсе по информационным технологиям,
хеш-функция не гарантирует взаимно-однозначного соответствия между
идентификаторами процессов и табличными индексами. Когда два разных
идентификатора процессов отображаются в один индекс таблицы, говорят,
что они конфликтуют. Для решения проблемы конфликтующих идентифика-
идентификаторов процессов в Linux используются цепные списки. Каждая запись табли-
таблицы является головой двунаправленного списка конфликтующих идентифика-
идентификаторов процессов. На рис. 3.5 изображена хеш-таблица идентификаторов про-
процессов, имеющая два списка. Процессы с идентификаторами 2890 и 29 384
отображаются в двухсотый элемент таблицы, а процесс с идентификатором
29 385 — в элемент 1466.
Рис. 3.5. Простая хеш-таблица идентификаторов процессов и цепные списки
Хеширование с применением цепных списков предпочтительнее линейного
преобразования идентификаторов процессов в табличные индексы, потому
что в любой момент времени количество процессов в системе, как правило,
намного меньше, чем 32 768 (максимальное количество допустимых иденти-
идентификаторов процессов). Было бы неразумно тратить память на таблицу из
32 768 записей, если в каждый момент времени большинство из них останет-
останется неиспользованным.
Структуры данных, используемые в хеш-таблицах идентификаторов процес-
процессов, довольно сложны, потому что они должны отражать взаимоотношения
между процессами. В качестве примера предположим, что ядро должно вы-
выбрать все процессы, принадлежащие данной группе потоков, т. е. все процес-
процессы, у которых поле tgid равно заданному числу. Поиск в хеш-таблице по за-
заданному номеру группы потоков возвратит только один дескриптор процесса,
а именно дескриптор лидера группы потоков. Чтобы быстро получить ос-
остальные процессы в группе, ядро должно поддерживать список процессов для
каждой группы потоков. Аналогичная ситуация возникает при поиске про-
процессов, принадлежащих заданному сеансу или заданной группе процессов.
Структуры данных в хеш-таблицах решают все эти проблемы, потому что
они позволяют определить список процессов для любого идентификатора
процесса, включенного в хеш-таблицу. Основной структурой является массив
из четырех структур pid, встроенный в поле pids дескриптора процесса. Поля
структуры pid перечислены в табл. 3.6.
На рис. 3.6 изображен пример, основанный на хеш-таблице pidtypetgid.
Второй элемент массива pidhash хранит адрес хеш-таблицы, т. е. массива
Таблица 3.6. Поля структуры рid
Тип Имя Описание
int nr Идентификатор процесса
struct hlist_node pid_chain Ссылки на следующий и предыдущий элементы
в цепном списке хеш-таблицы
struct listhead pidlist Голова отдельного списка для каждого иденти-
идентификатора процесса
Рис. 3.6. Хеш-таблицы идентификаторов процессов
структур hiisthead, являющихся головами цепных списков. В цепном спи-
списке, растущем из 71-й записи хеш-таблицы, находятся два дескриптора про-
процессов, соответствующие идентификаторам процессов 246 и 4351 (двойные
стрелки представляют пары указателей вперед/назад). Идентификаторы про-
цессов хранятся в поле пг структуры pid? встроенной в дескриптор процесса
(кстати, поскольку номер группы потоков совпадает с идентификатором про-
процесса-лидера, эти числа также хранятся в поле pid дескриптора процесса).
Рассмотрим список для группы с идентификатором 4351. Голова этого списка
хранится в поле pidiist дескриптора процесса, включенного в хеш-таблицу,
а ссылки на предыдущий и следующий элементы списка хранятся также и
в поле pidiist каждого элемента списка.
Для работы с хеш-таблицами идентификаторов процессов применяются сле-
следующие функции и макросы:
□ do_each_task_pid(nr, type, task) И while_each_task_pid(nr, type,
task) — отмечают начало и конец цикла do-while, который перебирает
список, ассоциированный с идентификатором процесса пг, имеющим тип
type. На каждом шаге цикла переменная task указывает на дескриптор
процесса для текущего элемента;
□ find_task_by_pid_type(type, nr) — эта функция ищет Процесс, ИМеЮЩИЙ
идентификатор пг, в таблице типа type. Функция возвращает указатель на
дескриптор процесса, если поиск завершился успешно, или null в против-
противном случае;
□ find_task_by_pid(nr) — те же действия, ЧТО find_task_by_pid_
type(PIDTYPE_PID, nr);
□ attach_pid(task, type, nr) — заносит дескриптор процесса, на который
указывает аргумент task, в хеш-таблицу идентификаторов процессов,
имеющую тип type, с учетом идентификатора процесса пг. Если дескрип-
дескриптор процесса, имеющего идентификатор пг, уже находится в таблице,
функция просто заносит процесс task в принадлежащий ему список;
□ detach_pid(task, type) — удаляет дескриптор процесса, на который ука-
указывает аргумент task, из отдельного списка типа type, содержащего этот
дескриптор. Если список не становится пустым, функция завершает рабо-
работу. В противном случае она удаляет дескриптор процесса из хеш-таблицы
типа type. И, наконец, если идентификатор процесса отсутствует во всех
остальных хеш-таблицах, функция сбрасывает соответствующий бит в би-
битовой карте идентификаторов процессов, чтобы этот идентификатор мож-
можно было использовать повторно;
□ nextthread(task) — возвращает адрес дескриптора облегченного процес-
процесса, следующего за дескриптором процесса task в хеш-таблице типа
pidtypetgid. Поскольку список в хеш-таблице является циклическим, то,
будучи вызванным для обычного процесса, этот макрос возвращает адрес
дескриптора самого процесса.
Как организованы процессы
В списках очереди на выполнение собраны все процессы, имеющие состоя-
состояние taskrunning. Когда возникает необходимость сгруппировать процессы,
находящиеся в других состояниях, выясняется, что разные состояния требуют
разного подхода, и Linux стоит перед следующим выбором:
□ Процессы в состояниях task_stopped, exit_zombie и exit_dead не объеди-
объединяются в специальные списки. Нет никакой необходимости группировать
эти процессы, поскольку остановленные, "зомбированные" и "мертвые"
процессы доступны только через их идентификаторы или связные списки
потомков конкретного родителя.
□ Процессы в состояниях task_interruptible или task_uninterruptible под-
подразделяются на много классов, каждый из которых соответствует опреде-
определенному событию. В данном случае состояние процесса не обеспечивает
достаточно информации, чтобы можно было быстро найти процесс, и по-
поэтому возникает необходимость в дополнительных списках процессов.
Они называются очередями ожидания и обсуждаются в следующем раз-
разделе.
Очереди ожидания
Очереди ожидания по-разному применяются в ядре; в частности, они исполь-
используются при обработке прерываний, синхронизации процессов и в хрономет-
хронометрировании. Поскольку эти темы затрагиваются в следующих главах, здесь мы
просто скажем, что процессу часто приходится ждать, пока наступит некото-
некоторое событие, например, завершится дисковая операция, освободится систем-
системный ресурс или истечет заданный интервал времени. Очереди ожидания реа-
реализуют условное ожидание событий: процесс, которому нужно ждать насту-
наступления какого-то события, встает в соответствующую очередь и освобождает
процессор. Таким образом, очередь ожидания представляет собой множество
спящих процессов, которые ядро разбудит, когда определенное условие будет
удовлетворено.
Очереди ожидания реализованы в виде двунаправленных списков, элементы
которых содержат указатели на дескрипторы процессов. Каждая очередь
ожидания идентифицируется головой очереди ожидания, структурой типа
wa it_queue_head_t I
struct wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct wait_queue_head wait_queue_head_t;
Поскольку очереди ожидания модифицируются обработчиками прерываний и
основными функциями ядра, двунаправленные списки должны быть защище-
защищены от попыток одновременного обращения, которые могут привести к не-
непредсказуемым результатам (см. главу 5). Синхронизация достигается с по-
помощью спин-блокировки lock, хранящейся в голове очереди. Поле taskiist
является головой списка ожидающих процессов.
Элементы списка в очереди ожидания имеют тип waitqueuet:
struct wait_queue {
unsigned int flags;
struct task_struct * task;
wait_queue_func_t func;
struct list__head task_list;
};
typedef struct wait_queue wait_queue_t;
Каждый элемент этого списка представляет спящий процесс, который ждет
наступления некоторого события. Адрес его дескриптора хранится в поле
task. Поле taskiist содержит указатели, связывающие этот элемент со спи-
списком процессов, ожидающих наступления того же события.
Однако будить все процессы в очереди ожидания не всегда удобно. Напри-
Например, если два или более процессов ожидают исключительного доступа к од-
одному и тому же ресурсу, имеет смысл будить только один из них. Этот про-
процесс захватит ресурс, а остальные продолжат спать. (Так удается избежать
проблемы "стада перед грозой", которая возникает, когда несколько процес-
процессов пробуждается только для того, чтобы поучаствовать в борьбе за ресурс,
к которому сможет обратиться только один из них, а остальным придется
вернуться ко сну.)
Таким образом, можно выделить два типа спящих процессов: эксклюзивные
процессы (помеченные единицей в поле flags соответствующего элемента
очереди ожидания) избирательно будятся ядром, а не эксклюзивные (поме-
(помеченные нулем в поле flags) будятся всегда, как только наступает ожидаемое
событие. Процесс, ожидающий ресурсы, который может быть выделен толь-
только одному процессу за раз, является типичным примером эксклюзивного
процесса. Процессы, ожидающие события, которое касается любого из них,
являются не эксклюзивными. Рассмотрим в качестве примера группу процес-
процессов, ожидающих окончания операций ввода/вывода. Когда пересылка данных
закончится, все ожидающие процессы могут быть разбужены. Как мы увидим
далее, поле func элемента очереди ожидания служит для уточнения, как
должны быть разбужены процессы, стоящие в очереди.
Работа с очередями ожидания
Новую голову очереди ожидания можно создать с помощью макроса
DECLARE_WAiT_QUEUE_HEAD(name), который статически объявляет новую пере-
менную с именем name в качестве головы очереди ожидания и инициализиру-
инициализирует ее ПОЛЯ lock И task_list. Функция init_waitqueue_head () МОЖет быть ИС-
пользована для инициализации головы очереди ожидания, для которой пере-
переменная была выделена динамически.
Функция init_waitqueue_entry(q,p) инициализирует структуру q типа
waitqueuet Следующим образом:
q->flags = 0;
q->task = р;
q->func = default_wake_function;
He эксклюзивный процесс р будет разбужен функцией default_wake_
functionO, которая является интерфейсной функцией для trytowakeupO
(см. главу 7).
Альтернативный макрос definewait объявляет новую переменную
waitqueuet и инициализирует ее дескриптором текущего процесса и адре-
адресом функции autoremovewakef unction (), ИСПОЛЬЗуемоЙ ДЛЯ Пробуждения.
Эта функция вызывает функцию defauitwakefunctiono, чтобы разбудить
спящий процесс, а затем удаляет соответствующий элемент из списка очере-
очереди на ожидание. Наконец, разработчик ядра может определить собственную
функцию для пробуждения процесса, проинициализировав элемент очереди
ОЖИДаНИЯ С ПОМОЩЬЮ фуНКЦИИ init_waitqueue_func_entry ().
После того как элемент определен, его нужно занести в очередь ожидания.
Функция addwaitqueueo ставит не эксклюзивный процесс на первое место
в списке очереди ожидания. Функция add_wait_queue_exciusive() ставит экс-
эксклюзивный процесс на последнее место в списке. Функция remove_
waitqueueO удаляет процесс из списка очереди ожидания, а функция
waitqueueactive () проверяет, пуст ли заданный список очереди ожидания.
Процесс, собирающийся подождать наступления некоторого события, может
вызвать одну из следующих функций:
П функция sieepon () работает с текущим процессом:
void sleep_on(wait_queue_head_t *wq)
{
wait_queue_t wait;
init_waitqueue_entry(&wait, current);
current->state = TASK_UNINTERRUPTIBLE;
add_wait_queue(wq,&wait); /* wq указывает на голову очереди
ожидания */
schedule();
remove_wait_queue(wq, &wait);
}
Функция переводит текущий процесс в состояние taskuninterruptible и
заносит его в указанную очередь ожидания. Затем она вызывает плани-
планировщик, который возобновляет выполнение другого процесса. Когда спя-
спящий процесс будет разбужен, планировщик возобновит выполнение функ-
функции sieepon (), которая удалит процесс из очереди ожидания;
□ функция interruptible_sleep_on() идентична функции sleep_on(), за
исключением того, что она переводит процесс в состояние task_
interruptible, а не taskjjninterruptible, чтобы процесс можно было раз-
будить и с помощью сигнала;
□ функции sleep_on_timeout () И interruptible_sleep_on_timeout () анало-
гичны предыдущим, но они также позволяют вызвавшему процессу опре-
определить временной интервал, по истечении которого ядро должно разбу-
разбудить Процесс. С ЭТОЙ ЦеЛЬЮ ОНИ ВЫЗЫВаЮТ фуНКЦИЮ schedule_timeout (), а
не schedule () (см. разд. "Применение динамических таймеров: системный
вызов nanosleepQ " главы 6);
□ функции prepare_to_wait (), prepare_to_wait_exclusive () И f inish_wait (),
впервые появившиеся в Linux 2.6, предлагают еще один способ перевода
процесса в состояние сна и занесения его в очередь ожидания. Вот их ти-
типичное применение:
DEFINE_WAIT(wait);
prepare_to_wait_exclusive(&wq, &wait, TASKJENTERRUPTIBLE);
/* wq — голова очереди ожидания */
if ({condition)
schedule();
finish_wait(&wq, &wait);
Функции prepare_to_wait () И prepare_to_wait_exclusive () переводят ПрО-
цесс в состояние, определяемое третьим параметром, а затем устанавли-
устанавливают флаг эксклюзивности в элементе очереди ожидания в значение 0 (не
эксклюзивный) или 1 (эксклюзивный) соответственно. Наконец, они зано-
заносят элемент wait очереди ожидания в список, определяемый головой оче-
очереди wq.
Сразу после своего пробуждения процесс вызывает функцию finish_
wait (), которая переводит его обратно в состояние taskrunning (на тот
случай, если условие пробуждения будет выполнено до вызова функции
schedule ()), и удаляет соответствующий элемент из списка очереди на вы-
выполнение (если это еще не было сделано функцией, разбудившей процесс);
□ макросы waitevent И waiteventinterruptible переводят вызвавший
процесс в состояние сна и заносят его в очередь ожидания, пока указанное
условие не будет удовлетворено. Например, макрос waitevent (wq, condition)
развертывается в следующий фрагмент:
DEFINE_WAIT ( wait) ;
for (;;) {
prepare_to_wait (&wq, & wait, TASK_UNINTERRUPTIBLE) ;
if (condition)
break;
schedule();
}
finish_wait(&wq, & wait);
Сделаем несколько замечаний по поводу перечисленных функций. Функции,
аналогичные sieepono, не могут быть использованы в типичной ситуации,
когда необходимо проверить условие и атомарным образом перевести про-
процесс в состояние сна, если условие не удовлетворено. По этой причине, учи-
учитывая, что они хорошо известны как источник конфликтных ситуаций, их
применение не рекомендуется.
Кроме того, чтобы поставить эксклюзивный процесс в очередь ожидания,
ядро ДОЛЖНО вызвать фуНКЦИЮ prepare_to_wait_exclusive() (ИЛИ непОСредСТ-
венно функцию addwaitqueueexciusiveo). Любая другая вспомогательная
функция поставит процесс в очередь как не эксклюзивный. Наконец, если не
используется ни define_wait, ни finish_wait о, ядро должно удалить элемент
из списка очереди ожидания после пробуждения процесса.
Ядро будит процессы в очередях ожидания, переводя их в состояние
TASKRUNNING С ПОМОЩЬЮ ОДНОГО ИЗ СЛедуЮЩИХ Макросов: wakeup, wakeupnr,
wake_up_all, wake_up_interruptible, wake_up_interruptible_nr, wake up
interruptible_all, wake_up_interruptible_sync И wake_up_locked. ПОНЯТЬ, ЧТО
делает каждый из них, можно по их именам:
□ все макросы работают со спящими процессами, находящимися в состоя-
состоянии taskinterruptible. Если имя макроса не содержит строку
"interruptible", он работает также и с процессами в taskuninterruptible;
□ все макросы будят все не эксклюзивные процессы, находящиеся в требуе-
требуемом состоянии;
О макросы, имена которых содержат строку мпгм, будят заданное количество
эксклюзивных процессов, находящихся в требуемом состоянии (это коли-
чество передается в качестве параметра макроса). Макросы, имена кото-
которых содержат строку "all", будят все эксклюзивные процессы, находящие-
находящиеся в требуемом состоянии. Наконец, макросы, имена которых не содержат
ни "пг", ни "all", будят только один эксклюзивный процесс, находящийся
в требуемом состоянии;
□ макросы, имена которых не содержат строку "sync", сравнивают приори-
приоритеты разбуженных процессов с приоритетами процессов, выполняемых в
системе в этот момент, и вызывают функцию schedule о, если необходи-
необходимо. Макросы, имена которых содержат строку "sync", такую проверку не
делают, и, следовательно, выполнение разбуженного процесса с высоким
приоритетом может быть немного задержано;
□ макрос wakeupiocked аналогичен макросу wakeup с тем отличием, что он
вызывается, когда спин-блокировка в структуре waitqueueheadt уже за-
захвачена.
Например, макрос wakeup фактически эквивалентен следующему фрагменту
кода:
void wake_up(wait_queue_head_t *q)
{
struct list_head *tmp;
wait queue_t *curr;
list_for_each(tmp, &q->task_list) {
curr = list_entry(trap, wait_queue_t, task_list);
if (curr->func(curr, TASK_INTERRUPTIBLE|TASK_UNINTERRUPTIBLE,
0, NULL) && curr->flags)
break;
}
}
Макрос listforeach перебирает все элементы двунаправленного списка
q->task_iist, т. е. все процессы в очереди ожидания. Для каждого элемен-
элемента макроса listentry вычисляет адрес соответствующей переменной
waitqueuet. Поле func этой переменной содержит адрес пробуждающей
функции, которая пытается разбудить процесс, идентифицируемый полем
task элемента очереди ожидания. Если процесс действительно был разбужен
(функция возвратила 1), и он является эксклюзивным (поле curr->f lags со-
содержит 1), цикл заканчивается. Поскольку не эксклюзивные процессы всегда
находятся в начале двунаправленного списка, а все эксклюзивные — в конце,
функция всегда будит не эксклюзивные процессы, а затем один эксклюзив-
эксклюзивный, если таковой имеется4.
4 Между прочим, довольно редко бывает, что очередь ожидания содержит как эксклюзивные, так и
не эксклюзивные процессы.
Ограничения на ресурсы процесса
У каждого процесса есть ограничения на ресурсы, которые определяют объем
системных ресурсов, предоставляемых ему в пользование. Эти ограничения
не позволяют пользователю злоупотреблять системными ресурсами (процес-
(процессорным временем, дисковым пространством и т. д.). Linux поддерживает ог-
ограничения на ресурсы, приведенные в табл. 3.7.
Ограничения на ресурсы для текущего процесса хранятся в поле current->
signai->riim, т. е. в поле дескриптора сигнала этого процесса (см.
разд. "Структуры данных, ассоциированные с сигналами" главы 11). Это по-
поле представляет собой массив элементов типа struct riimit, по одному на
каждое ограничение ресурса:
struct riimit {
unsigned long rlim_cur;
unsigned long rlim_max;
};
Таблица 3.7. Ограничения на ресурсы
Имя поля Описание
rlimit_as Максимальный размер адресного пространства процесса, в бай-
байтах. Ядро проверяет это значение, когда процесс вызывает
функцию mallocO или аналогичную ей, чтобы увеличить свое
адресное пространство (см. разд. "Адресное пространство про-
процесса" главы 9)
rlimit_core Максимальный размер файлов core-дампа, в байтах. Ядро про-
проверяет это значение, когда процесс завершается принудительно,
до создания файла core в текущем каталоге процесса (см.
разд. "Действия, выполняемые при доставке сигнала" гла-
главы 11). Если значение равно 0, ядро не станет создавать файл
rlimit_cpu Максимальное процессорное время, уделяемое процессу, в се-
секундах. Если процесс превышает этот лимит, ядро посылает ему
сигнал sigxcpu, а затем, если процесс не заканчивает работу —
сигнал sigkill (см. главу 11)
rlimit_data Максимальный размер кучи, в байтах. Ядро проверяет это зна-
значение перед расширением кучи процесса (см. разд. "Управление
кучей" главы 9)
rlimitfsize Максимальный разрешенный размер файла, в байтах. Если про-
процесс попытается расширить файл до размера, превышающего
это значение, ядро посылает ему сигнал sigxfsz
rlimit_locks Максимальное количество файловых блокировок (в настоящее
время ограничение не действует)
Таблица 3.7 (окончание)
Имя поля Описание
rlimit_memlock Максимальный размер невыгружаемой памяти, в байтах. Ядро
проверяет это значение, когда процесс пытается заблокировать
страничный кадр в памяти с помощью системных вызовов
mlock() или mlockalK) (см. разд. "Выделение интервала ли-
линейных адресов" главы 9)
rlimit_msgqueue Максимальное количество байтов в очереди сообщений POSIX
(см. разд. "Очереди сообщений POSIX" главы 19)
rlimit_nofile Максимальное количество дескрипторов открытых файлов. Ядро
проверяет это значение, когда открывает новый файл или копи-
копирует дескриптор файла (см. главу 12)
rlimit_nproc Максимальное количество процессов, которыми может владеть
данный пользователь
rlimit_rss Максимальное количество страничных кадров, которыми может
владеть процесс (в настоящее время ограничение не действует)
rlimit_sigpending Максимальное количество сигналов, ожидающих обработки,
у процесса (см. главу 11)
rlimit_stack Максимальный размер стека, в байтах. Ядро проверяет это зна-
значение перед расширением стека процесса в режиме пользовате-
пользователя (см. главу 9)
Поле riimcur содержит текущее ограничение на ресурс. Например, поле
current->signai->riirn[RLiMiT_cpu] .riim_cur представляет ограничение на
процессорное время для текущего процесса.
Поле riimmax содержит максимальное значение для ограничения на ресурс.
С помощью системных вызовов getriimito и setriimito пользователь все-
всегда может увеличить предел riimcur для некоторого ресурса до значения
riimmax. Однако только суперпользователь (точнее говоря, пользователь,
обладающий возможностью capsysresource) может увеличить значение в
поле riimmax или записать в поле riimcur значение, превышающее riimmax.
Большинство ограничений на ресурсы имеет значение rliminfinity
(Oxffffffff), означающее, что на использование этого ресурса никакое огра-
ограничение не накладывается (конечно, реальные ограничения все равно суще-
существуют в зависимости от конструктивных особенностей ядра, объема опера-
оперативной памяти, свободного места на диске и т. д.). Тем не менее системный
администратор может наложить на некоторые ресурсы определенные ограни-
ограничения. Когда пользователь входит в систему, ядро создает принадлежащий
суперпользователю процесс, который может сделать системный вызов
setrlimit О, ЧТОбы умеНЬШИТЬ Значения В ПОЛЯХ riimmax И riimcur ДЛЯ ДаН-
ного ресурса. Этот же процесс впоследствии выполняет оболочку и перехо-
дит во владение пользователя. Каждый новый процесс, созданный пользова-
пользователем, наследует от своего родителя содержимое массива rlim, и, следова-
следовательно, пользователь не может переопределить ограничения, установленные
администратором.
Переключение процессов
Чтобы управлять выполнением процессов, ядро должно уметь приостанавли-
приостанавливать процесс, работающий в данный момент, и возобновлять выполнение
другого, ранее приостановленного процесса. Этот вид деятельности называ-
называется по-разному: переключение процессов, переключение задач или пере-
переключение контекста. В следующих разделах излагаются основы переключе-
переключения процессов в Linux.
Аппаратный контекст
В то время как адресное пространство у каждого процесса свое, всем процес-
процессам приходится делить между собой регистры процессора. Поэтому, прежде
чем возобновлять выполнение процесса, ядро должно загрузить в каждый
такой регистр значение, которое было в нем, когда выполнение процесса бы-
было приостановлено.
Набор данных, который должен быть загружен в регистры до возобновления
работы процесса, называется аппаратным контекстом. Аппаратный кон-
контекст является подмножеством контекста выполнения процесса, включающе-
включающего в себя всю информацию, необходимую для выполнения процесса. В Linux
часть аппаратного контекста процесса хранится в дескрипторе процесса, а
остальная его часть — в стеке режима ядра.
Далее в тексте мы подразумеваем, что локальная переменная prev ссылается
на дескриптор процесса, с которого происходит переключение, а переменная
next — на дескриптор замещающего процесса, т. е. процесса, на который
происходит переключение. Тогда мы может определить переключение про-
процессов как действия по сохранению аппаратного контекста процесса prev и
замене его на аппаратный контекст процесса next. Поскольку переключение
процессов имеет место довольно часто, важно минимизировать время сохра-
сохранения и загрузки аппаратных контекстов.
В старых версиях Linux использовалась аппаратная поддержка, предлагаемая
архитектурой 80x86. Переключение процессов производилось с помощью
инструкции far jmp5, выполнявшей переход на селектор дескриптора сегмен-
5 Инструкции far jmp модифицируют регистры cs и eip, а простые инструкции jmp — только
регистр eip.
та состояния (задачи), принадлежащего процессу next. При выполнении этой
инструкции процессор производит переключение аппаратного контекста, ав-
автоматически сохраняя старый аппаратный контекст и загружая новый. Одна-
Однако в Linux 2.6 применяется программное переключение процессов, и тому
есть причины:
□ пошаговое переключение, выполняемое с помощью последовательности
инструкций mov, обеспечивает более строгий контроль допустимости за-
загружаемых данных. В частности, имеется возможность проверить значе-
значения сегментных регистров ds и es, которые могут быть подделаны злона-
злонамеренным пользователем. Такой тип проверки невозможен, когда приме-
применяется одиночная инструкция far jmp;
D время, необходимое для переключения, приблизительно одинаковое как
при старом подходе, так и при новом. Но если оптимизировать аппарат-
аппаратный контекст невозможно, то для улучшения теперешнего кода переклю-
переключения возможности еще есть.
Переключение процессов происходит только в режиме ядра. Содержимое
всех регистров, использованных процессом в режиме пользователя, уже со-
сохранено в стеке режима ядра до переключения (см. главу 4). Это относится и
к содержимому пары регистров ss и esp, которая определяет адрес указателя
на стек режима пользователя.
Сегмент состояния задачи
В архитектуре 80x86 имеется специальный тип сегмента для хранения аппа-
аппаратных контекстов — сегмент состояния задачи, или сегмент TSS (Task State
Segment). Хотя операционная система Linux не применяет аппаратное пере-
переключение контекста, она все же вынуждена устанавливать сегмент состояния
задачи для каждого процессора в системе. Это делается, в основном, по двум
причинам:
□ когда процессор 80x86 переключается из пользовательского режима в
режим ядра, он извлекает адрес стека режима ядра из сегмента TSS
(см. разд. "Аппаратная обработка прерываний и исключений" главы 4 и
разд. "Выдача системного вызова с помощью инструкции sysenter" гла-
главы 10);
□ когда процесс в пользовательском режиме пытается обратиться к порту
ввода/вывода с помощью инструкции in или out, процессору, возможно,
понадобится обратиться к битовой карте разрешений ввода/вывода, хра-
хранящейся в сегменте состояния задачи, чтобы убедиться, что процессу раз-
разрешено обращаться к этому порту; точнее говоря, когда процесс выполня-
ет в пользовательском режиме инструкцию ввода/вывода in или out,
управляющий блок производит следующие действия:
• проверяет двухбитовое поле iopl в регистре efiags. Если оно содер-
содержит 3, управляющий блок выполняет инструкции ввода/вывода. В про-
противном случае он делает следующую проверку;
• читает регистр tr, чтобы определить текущий сегмент состояния задачи
и, следовательно, нужную битовую карту разрешений ввода/вывода;
• в битовой карте разрешений ввода/вывода проверяет бит, соответст-
соответствующий порту ввода/вывода, указанному в инструкции. Если бит
сброшен, инструкция выполняется; в противном случае управляющий
блок возбуждает исключение "Общий сбой защиты".
Структура tssstruct описывает формат сегмента TSS. Как было сказано в
главе 2, массив inittss хранит по одному сегменту TSS на каждый процес-
процессор в системе. При переключении процессов ядро обновляет некоторые поля
сегмента TSS, чтобы управляющий блок соответствующего процессора мог
без опасений читать необходимую ему информацию. Таким образом, сегмент
состояния задачи отражает привилегии текущего процесса на процессоре, но
при этом нет необходимости сопровождать TSS-сегменты процессов, когда
они не работают.
У каждого сегмента состояния задачи есть свой восьмибайтовый дескриптор.
Он включает в себя 32-битовое поле Base, которое указывает на начало сег-
сегмента TSS, и 20-битовое поле Limit. Флаг s дескриптора TSS-сегмента сбро-
сброшен, чтобы отметить тот факт, что соответствующий TSS-сегмент является
системным сегментом (см. главу 2).
Поле туре равно или 9, или и. Это означает, что сегмент является сегментом
состояния задачи. Согласно оригинальному подходу Intel, каждый процесс в
системе должен иметь собственный сегмент состояния задачи, и второй
младший бит поля туре называется битом занятости. Он равен единице, ко-
когда процесс выполняется на процессоре, и нулю в противном случае. Соглас-
Согласно подходу Linux, у каждого процессора есть только один сегмент состояния
задачи, и бит занятости всегда равен 1.
Дескрипторы сегментов TSS, созданных Linux, хранятся в глобальной табли-
таблице дескрипторов (Global Descriptor Table, GDT), базовый адрес которой хра-
хранится в регистре gdtr каждого процессора. Регистр tr каждого процессора
содержит селектор дескриптора соответствующего сегмента TSS. Этот ре-
регистр также имеет два скрытых непрограммируемых поля, поля Base и Limit
дескриптора сегмента TSS. Это позволяет процессору обращаться к сегменту
состояния задачи напрямую, не извлекая его адрес из глобальной таблицы
дескрипторов.
Поле thread
При каждом переключении процессов аппаратный контекст замещаемого
процесса должен быть где-то сохранен. Он не может быть сохранен в сегмен-
сегменте состояния задачи, что соответствовало бы подходу Intel, т. к. в Linux ис-
используется один сегмент состояния задачи для всех процессов, а не отдель-
отдельный сегмент для каждого процесса.
Поэтому дескриптор процесса включает в себя поле thread типа thread_
struct, в котором ядро сохраняет аппаратный контекст, когда происходит
переключение с этого процесса на другой. Как мы увидим далее, эта структу-
структура имеет поля почти для всех регистров процессора, кроме регистров общего
назначения еах, ebx и др., которые сохраняются в стеке режима ядра.
Выполнение переключения процессов
Переключение процессов может произойти только в одном, строго опреде-
определенном месте — в теле функции schedule (), которая подробно обсуждается в
главе 7. Здесь мы ограничимся описанием того, как ядро выполняет переклю-
переключение процессов.
В сущности, каждое такое переключение состоит из двух шагов:
1. Переключение глобального каталога страниц с целью установки адресного
пространства. Этот этап подробно обсуждается в главе 9.
2. Переключение стека режима ядра и аппаратного контекста. Этот шаг
обеспечивает ядро информацией, необходимой для выполнения нового
процесса, в том числе содержимым регистров процессора.
Мы по-прежнему будем предполагать, что prev указывает на дескриптор за-
замещаемого процесса, a next — на дескриптор активизируемого процесса. Как
мы увидим в главе 7, prev и next являются локальными переменными функ-
функции schedule().
Макрос switchjto
Второй шаг переключения процессов выполняется макросом switchto. Это
одна из наиболее аппаратно-зависимых процедур ядра, и для понимания ее
действий требуются некоторые усилия.
Во-первых, макрос принимает три параметра, prev, next и last. Нетрудно до-
догадаться о назначении параметров prev и next: они играют ту же роль, что и
локальные переменные prev и next, т. е. являются входными параметрами,
задающими ячейки памяти, хранящие адреса дескрипторов замещаемого
процесса и замещающего процесса соответственно.
А как насчет третьего параметра, last? Дело в том, что в каждое переключе-
переключение процессов вовлечены не два, а три процесса. Предположим, ядро приняло
решение переключиться с процесса А на процесс В. В функции schedule!)
переменная prev указывает на дескриптор процесса A, a next — на дескрип-
дескриптор процесса В. Как только макрос switchto деактивирует процесс А, его
выполнение прекращается.
Впоследствии, когда ядро решит возобновить выполнение процесса А, оно
должно будет приостановить некоторый процесс С (вообще говоря, отличный
от В), для чего вызовет макрос switchto с параметром prev, указывающим
на С, и параметром next, указывающим на А. Когда процесс А продолжит
работу, он найдет свой старый стек режима ядра, где локальная переменная
prev указывает на дескриптор процесса А, а переменная next указывает на
дескриптор процесса В. Планировщик, который теперь выполняется от имени
процесса А, не имеет ссылки на процесс С. Однако оказывается, что эта
ссылка нужна для завершения процедуры переключения процессов (подроб-
(подробности см. в главе 7).
Последний параметр макроса switchto является выходным и задает ячейку
памяти, в которую макрос запишет адрес дескриптора процесса С (естествен-
(естественно, это делается после того, как процесс А возобновит выполнение). Перед
переключением процессов макрос сохраняет в процессорном регистре еах
содержимое переменной, идентифицируемой первым входным параметром
prev, т. е. содержимое локальной переменной prev, хранящейся в стеке режи-
режима ядра, принадлежащем процессу А. После переключения процессов, когда
процесс А возобновляет работу, макрос записывает содержимое регистра еах
в ячейку памяти процесса А, идентифицируемую третьим выходным пара-
параметром last. Поскольку содержимое этого регистра не меняется по ходу пе-
переключения процессов, в эту ячейку записывается адрес дескриптора процес-
процесса С. В текущей реализации функции schedule о последний параметр иден-
идентифицирует локальную переменную prev процесса А, так что prev
переписывается адресом процесса С.
Содержимое стеков режима ядра процессов А, В и С, а также значения реги-
регистра еах изображены на рис. 3.7. Необходимо учитывать, что на рисунке по-
показано значение локальной переменной prev до того, как оно будет переписа-
переписано содержимым регистра еах.
Макрос switchto написан на расширенном встроенном ассемблере, который
довольно трудно читать. В частности, ссылки на регистры делаются с по-
помощью специальной позиционной нотации, которая позволяет компилятору
самостоятельно решать, какими регистрами общего назначения он будет
пользоваться. Вместо того чтобы приводить здесь громоздкий код на расши-
расширенном встроенном ассемблере, мы опишем работу макроса switchto в архи-
тектуре 80x86, пользуясь стандартным ассемблером. Итак, макрос switchto
выполняет следующие действия:
1. Сохраняет значения параметров prev и next в регистрах еах и edx соответ-
соответственно:
movl prev, %eax
movl next, %edx
2. Сохраняет содержимое регистров ef lags и ebp в стеке режима ядра про-
процесса prev. Эти регистры нужно сохранить, потому что компилятор пред-
предполагает их неизменность вплоть до окончания макроса switchto:
pushfl
pushl %ebp
3. Сохраняет содержимое регистра esp в поле prev->thread.esp, чтобы это
поле указывало на верхушку стека режима ядра процесса prev:
movl %esp,484(%еах)
Операнд 484(%еах) идентифицирует ячейку памяти, адрес которой равен
содержимому регистра еах, увеличенному на 484.
4. Загружает значение next->thread.esp в регистр esp. С этого момента ядро
работает со стеком режима ядра процесса next, т. е. эта инструкция фак-
фактически выполняет переключение от процесса prev к процессу next. По-
Поскольку адрес дескриптора процесса тесно связан с адресом стека режима
ядра, смена стека ядра равносильна смене текущего процесса.
movl 484(%edx), %esp
5. Сохраняет адрес с меткой 1 (о нем далее в этом разделе) в поле
prev->thread.eip. Когда замещаемый процесс возобновит свое выполне-
выполнение, он выполнит инструкцию с меткой i:
movl $lf, 480(%eax)
Рис. 3.7. Сохранение ссылки на процесс С во время переключения процессов
6. Заносит значение поля next->thread. eip в стек режима ядра процесса next.
В большинстве случаев это адрес с меткой i:
pushl 480(%edx)
7. Переходит на выполнение функции switchto (), написанной на языке С
(см. следующий раздел):
jmp switch_to
8. Здесь процесс А, который был замещен процессом В, снова получает в
свое распоряжение процессор. Выполняется ряд инструкций, восстанавли-
восстанавливающих содержимое регистров еflags и ebp. Первая инструкция имеет
метку 1:
1:
popl %ebp
popfl
Обратите внимание, что эти инструкции pop работают со стеком ядра,
принадлежащим процессу prev. Они будут выполнены, когда планиров-
планировщик выберет prev в качестве нового процесса, подлежащего выполнению,
и вызовет макрос switchto, передав ему prev в качестве второго парамет-
параметра. Таким образом, регистр esp указывает на стек режима ядра процесса
prev.
9. Копирует содержимое регистра еах (загруженное на шаге 1) в ячейку па-
памяти, идентифицируемую параметром last макроса switchto:
movl %eax, last
Как было сказано ранее, регистр еах указывает на дескриптор только что
замещенного процесса6.
Функция switch_to()
Функция switchto () выполняет основную часть работы по переключению
процессов, начатой макросом switchto (). Она принимает параметры prevp
и nextp, обозначающие старый и новый процессы соответственно. Вызов
этой функции отличается от вызова "среднестатистической" функции, по-
поскольку функция switchto () берет значения параметров prevp и nextp из
регистров еах и edx (в которых, как мы видели, эти значения сохраняются), а
не из стека, как большинство других функций. Чтобы заставить функцию
6 Как уже говорилось, в текущей реализации функции schedule () используется локальная пере-
переменная prev, так что соответствующая ассемблерная инструкция выглядит примерно так:
movl % еах, prev.
брать параметры из регистров, ядро применяет ключевые слова attribute
и regparm, являющиеся нестандартными расширениями языка С, реализован-
реализованными в компиляторе gcc. Функция switchto () объявлена в заголовочном
файле include/asm-i386/system.h следующим образом:
switch_to(struct task_struct *prev_p,
struct task_struct *next_p)
attribute (regparmC) ) ;
Она выполняет следующие действия:
1. Выполняет код, возвращенный макросом uniazyfpuO (см. разд. "Со-
"Сохранение и загрузка регистров FPU, ММХ и ХММ" далее в этой главе),
чтобы сохранить содержимое регистров FPU, ММХ и ХММ процесса
prevp, если необходимо.
unlazy_fpu(prev_p);
2. Выполняет макрос smpprocessorid (), чтобы получить индекс локального
процессора, т. е. процессора, выполняющего код. Макрос извлекает иско-
искомый индекс из поля ери структуры threadinf о текущего процесса и со-
сохраняет его в локальной переменной ери.
3. Загружает значение поля next_p->thread.espO в поле espO сегмента состоя-
состояния задачи, ассоциированного с локальным процессором. Как мы узнаем
из разд. "Выдача системного вызова с помощью инструкции sysenter" гла-
главы 10, любое последующее изменение уровня привилегий от режима поль-
пользователя до режима ядра, вызванное ассемблерной инструкцией sysenter,
приведет к копированию этого адреса в регистр esp:
init_tss[cpu].espO = next_p->thread.espO;
4. Загружает в глобальную таблицу дескрипторов локального процесса сег-
сегменты TLS (Thread-Local Storage), используемые процессом nextp. Три
селектора сегментов хранятся в массиве tisarray в дескрипторе процесса
(см. главу 2).
cpu_gdt_table[cpu][б] = next_p->thread.tls_array[0];
cpu_gdt_table[cpu][7] = next_p->thread.tls_array[1];
cpu_gdt_table[cpu][8] = next_p->thread.tls_array[2];
5. Сохраняет содержимое сегментных регистров fs и gs в полях prev_p->
thread, fs И prev_p->thread.gs, соответственно, ВЫПОЛНЯЯ Следующие ас-
семблерные инструкции:
movl %fs, 40(%esi)
movl %gs, 44 (%esi)
Регистр esi указывает на структуру prev_p->thread.
6. Если сегментный регистр fs или gs был уже использован процессом
prevp или nextp (то есть если они содержат ненулевое значение), функ-
функция загружает в эти регистры значения, хранящиеся в дескрипторе
threadstruct процесса nextp. Этот шаг логически дополняет действия,
выполненные в предыдущем. Главными ассемблерными инструкциями
при этом являются:
movl 40(%ebx),%fs
movl 44(%ebx),%gs
Регистр ebx указывает на структуру next_p->thread. На самом деле, код
намного сложнее, потому что процессор может возбудить исключение,
если он обнаружит недопустимое значение в сегментном регистре. Код
учитывает эту возможность, прибегая к приему "обработка исключения"
(см. разд. "Динамическая проверка адресов: код обработки исключения"
главы 10).
7. Загружает в отладочные регистры drO, ..., dr77 содержимое массива next_p->
thread, debugreg. Это делается ТОЛЬКО В ТОМ Случае, еСЛИ процесс nextp
пользовался отладочными регистрами в момент, когда его выполнение
было Приостановлено (ТО еСТЬ еСЛИ ПОЛе next_p->thread. debugreg [7] не
равно 0). В других случаях эти регистры сохранять необязательно, потому
что массив prev_p->thread. debugreg модифицируется, только когда отлад-
чику необходимо вести мониторинг процесса prev:
if (next_p->thread.debugreg[7]){
loaddebug(&next_p->thread, 0);
loaddebug(&next_p->thread, 1);
loaddebug(&next_p->thread, 2);
loaddebug(&next_p->thread, 3);
/* 4 и 5 отсутствуют */
loaddebug(&next_p->thread, 6);
loaddebug(&next_p->thread, 7);
}
8. Обновляет битовую карту ввода/вывода в сегменте состояния задачи, если
необходимо. Это нужно делать, если процесс nextp или prevp имеет соб-
собственную битовую карту разрешений ввода/вывода:
if (prev_p->thread.io_bitmap ptr || next_p->thread.io bitmap ptr)
handle_io_bitmap(&next_p->thread, &init_tss[cpu]);
7 Отладочные регистры процессоров 80x86 позволяют вести мониторинг процесса на аппаратном
уровне. Можно определить до четырех областей с точками останова. Когда процесс, подвергающий-
подвергающийся мониторингу, обращается к адресу, расположенному в одной из таких областей, возникает ис-
исключение.
Поскольку процессы редко модифицируют битовую карту разрешений
ввода/вывода, она обрабатывается в "ленивом" режиме. Реальная битовая
карта копируется в сегмент состояния задачи локального процессора,
только если процесс действительно обращается к порту ввода/вывода в те-
текущем интервале времени. Битовая карта разрешений ввода/вывода, при-
принадлежащая какому-то процессу, хранится в буфере, на который указыва-
указывает ПОЛе iobitmapptr Структуры threadinfo ЭТОГО Процесса. ФуНКЦИЯ
handieiobitmap () устанавливает значение в поле iobitmap сегмента со-
состояния задачи локального процессора для процесса nextp следующим
образом:
• если процесс nextp не имеет собственной битовой карты разрешений
ввода/вывода, в поле iobitmap сегмента состояния задачи записывает-
записывается значение Oxsooo;
• если у процесса nextp есть собственная битовая карта разрешений
ввода/вывода, в поле iobitmap сегмента состояния задачи записывает-
записывается значение 0x9ооо.
Поле iobitmap сегмента состояния задачи должно содержать смещение
внутри этого сегмента, определяющее место хранения реальной битовой
карты. Значения Oxsooo и 0x9000 указывают за пределы сегмента TSS, что
послужит причиной возбуждения исключения "Общий сбой защиты", как
только процесс режима пользователя попытается обратиться к порту вво-
ввода/вывода (см. разд. "Исключения" главы 4). Обработчик исключения
do_general_protection() проверит значение В ПОЛе iobitmap. Если ОНО
равно 0x8000, функция отправит сигнал sigsegv процессу в пользователь-
пользовательском режиме. Если же оно равно 0x9000, функция скопирует битовую кар-
карту процесса (на которую указывает поле iobitmapptr структуры
threadinfo) в сегмент состояния задачи локального процессора, запишет
в поле iobitmap фактическое смещение битовой карты A04) и форсирует
новое выполнение ассемблерной инструкции, приведшей к возбуждению
исключения.
9. Завершает работу. Функция switchto () (написанная на языке С) закан-
заканчивается оператором:
return prev_p;
Соответствующие ассемблерные инструкции, сгенерированные компиля-
компилятором, выглядят так:
movl %edi,%eax
ret
Параметр prevp (сейчас хранящийся в регистре edi) копируется в регистр
еах, потому что значение, возвращаемое любой функцией на языке С, пе-
редается через регистр еах. Обратите внимание, что таким образом значе-
значение регистра еах оказывается одинаковым до и после вызова функции
switchto (). Это очень важно, потому что при вызове макроса switchto
подразумевается, что регистр еах всегда хранит адрес дескриптора заме-
замещаемого процесса.
Ассемблерная инструкция ret загружает в счетчик команд eip адрес воз-
возврата, хранящийся на верхушке стека. Однако функция switchto () бы-
была вызвана простой инструкцией перехода. Поэтому инструкция ret нахо-
находит на верхушке стека адрес инструкции с меткой 1, который был положен
в стек макросом switchto. Если процесс nextp до этого не приостанавли-
приостанавливался, потому что выполняется впервые, функция находит начальный ад-
адрес функции retf romf ork ().
Сохранение и загрузка регистров FPU, ММХ и ХММ
Начиная с Intel 80486DX, блок операций с плавающей точкой (Floating Point
Unit, FPU) интегрирован в процессор. Тем не менее название математиче-
математический сопроцессор используется по-прежнему и напоминает о тех днях, когда
вычисления с плавающей точкой выполнялись дорогим специализированным
чипом. Для поддержки совместимости со старыми моделями арифметические
функции выполняются при помощи ESCAPE-инструкций, т. е. инструкций с
префиксным байтом, имеющим значение от 0xd8 до Oxdf. Эти инструкции
работают с набором процессорных регистров для операций с плавающей точ-
точкой. Очевидно, что если процесс использует ESCAPE-инструкции, то содер-
содержимое регистров с плавающей точкой является частью его аппаратного кон-
контекста и должно быть сохранено. Впоследствии компания Intel встроила в
свои микропроцессоры Pentium новый набор ассемблерных инструкций. Они
получили название ММХ-инструкций и были задуманы для ускорения работы
мультимедийных приложений. ММХ-инструкции используют регистры бло-
блока операций с плавающей точкой (блока FPU). Очевидным недостатком этого
архитектурного решения является невозможность для программистов приме-
применять инструкции с плавающей точкой вместе с ММХ-инструкциями. Досто-
Достоинство же заключается в том, что разработчики операционной системы могут
игнорировать новый набор инструкций, потому что фрагмент кода, переклю-
переключающего процессы, отвечающий за сохранение состояния блока FPU, может
быть использован и для сохранения состояния блока ММХ.
ММХ-инструкции ускоряют работу мультимедийных приложений, потому
что реализуют внутри процессора конвейер SIMD (Single-Instruction Multiple-
Data, "одна инструкция— множественные данные"). В модели Pentium III
возможности SIMD-конвейера расширяются за счет введения расширений
SSE (Streaming SIMD Extensions, потоковые расширения SIMD), которые по-
зволяют обрабатывать значения с плавающей точкой, хранящиеся в
128-битовых регистрах, называемых ХММ-регистрами. Такие регистры не
перекрываются регистрами FPU и ММХ, так что инструкции SSE можно
смело смешивать с инструкциями FPU/MMX. В модели Pentium 4 появилась
еще одна функциональная возможность— расширения SSE2. В принципе,
это SSE-расширения с поддержкой значений с плавающей точкой с повы-
повышенной точностью. Расширения SSE2 используют тот же набор ХММ-
регистров, что и расширения SSE.
Микропроцессоры 80x86 не сохраняют автоматически регистры FPU, MMX и
ХММ в сегменте состояния задачи. Однако они предоставляют некоторую
аппаратную поддержку, позволяющую ядру сохранять эти регистры только
тогда, когда это действительно необходимо. Поддержка сводится к флагу ts в
регистре его, причем соблюдаются следующие правила:
□ каждый раз, когда производится переключение аппаратного контекста,
флаг ts устанавливается;
□ каждый раз, когда инструкция ESCAPE, MMX, SSE или SSE2 выполняется
при установленном флаге ts, управляющий блок возбуждает исключение
"Устройство недоступно" (см. главу 4).
Флаг ts позволяет ядру сохранять и восстанавливать регистры FPU, MMX и
ХММ, только когда это действительно необходимо. В качестве иллюстрации
предположим, что процесс А пользуется математическим сопроцессором.
Когда происходит переключение контекста с А на В, ядро устанавливает флаг
ts и сохраняет регистры с плавающей точкой в сегменте состояния задачи,
которым пользуется процесс А. Если новый процесс В не работает с матема-
математическим сопроцессором, ядру не нужно будет восстанавливать содержимое
регистров с плавающей точкой. Но как только процесс В попытается выпол-
выполнить инструкцию ESCAPE или ММХ, процессор возбудит исключение "Уст-
"Устройство недоступно", и соответствующий обработчик загрузит в регистры
с плавающей точкой значения, сохраненные в сегменте состояния задачи про-
процесса В.
Теперь мы опишем структуры данных, применяемые при избирательной за-
загрузке регистров FPU, ММХ и ХММ. Они хранятся в дескрипторе процесса,
в субполе thread. i387, формат которого описывается объединением
i387 unionl
union i387 union {
struct i387_fsave_struct fsave;
struct i387_fxsave_struct fxsave;
struct i387_soft_struct soft;
};
Из описания видно, что поле может хранить структуры только одного из трех
типов. Тип i387_soft_struct применяется моделями процессоров без матема-
математического сопроцессора, и ядро Linux до сих пор поддерживает эти старые
чипы, эмулируя сопроцессор программным образом. Впрочем, мы больше не
будем обсуждать этот морально устаревший случай. Тип i387_fsave_struct
используется в процессорах, имеющих математический сопроцессор и, воз-
возможно, блок ММХ. Наконец, тип i387_fxsave_struct используется процессо-
процессорами, поддерживающими расширениями SSE и SSE2.
Дескриптор процесса содержит два дополнительных флага:
П флаг TSUSEDFPU, ВХОДЯЩИЙ В СОСТаВ ПОЛЯ status ДеСКрИПТОра thread_info.
Он показывает, использовал ли процесс регистры FPU, ММХ и ХММ в те-
текущем выполнении;
□ флаг PFUSEDMATH, ВХОДЯЩИЙ В СОСТаВ ПОЛЯ flags ДеСКрИПТОра taskstruct.
Этот флаг показывает, имеет ли смысл содержимое субполя thread.i387.
Флаг сбрасывается (содержимое смысла не имеет) в следующих двух слу-
случаях:
• когда процесс запускает новую программу, делая системный вызов
execveo (см. главу 20). Поскольку управление не будет возвращено
прежней программе, данные в поле thread.i387 никогда не понадо-
понадобятся;
• когда процесс, выполнявший программу в пользовательском режиме,
запускает процедуру обработчика сигнала (см. главу 11). Поскольку об-
обработчики сигналов асинхронны по отношению к программе, содержи-
содержимое регистров с плавающей точкой может быть бессмысленным для
обработчика сигнала. Тем не менее ядро сохраняет регистры с плаваю-
плавающей точкой в поле thread. i387 до начала выполнения обработчика и
восстанавливает их после его окончания. Поэтому обработчику сигнала
разрешается использовать математический сопроцессор.
Сохранение регистров FPU
Как было сказано ранее, функция switchto () выполняет макрос uniazy_
fpu, передавая ему в качестве аргумента дескриптор замещаемого процесса
prev. Макрос проверяет значение флага tsusedfpu у процесса prev. Если
флаг установлен, значит, процесс prev пользовался инструкциями FPU,
ММХ, SSE или SSE2, и ядро должно сохранить соответствующий аппарат-
аппаратный контекст:
if (prev->thread_info->status & TSJJSEDFPU)
save_init_fpu(prev);
Функция saveinitfpu () выполняет следующие действия:
1. Сохраняет содержимое регистров FPU в дескрипторе процесса prev, после
чего заново инициализирует блок FPU. Если процессор использует расши-
расширения SSE/SSE2, функция также сохраняет содержимое регистров ХММ и
заново инициализирует блок SSE/SSE2. Для реализации всего этого доста-
достаточно пары строчек на мощном расширенном встроенном ассемблере;
либо
asm volatile( "fxsave
%0 ; fnclex"
: "=m" (prev->thread.i387.fxsave) );,
если процессор использует расширения SSE/SSE2, либо
asm volatile( "fnsave
%0 ; fwait"
: "=m" (prev->thread.i387.fsave) );
в противном случае.
2. Сбрасывает флаг tsusedfpu процесса prev:
prev->thread_info->status &= -TSJJSEDFPU;
3. Устанавливает бит ts в регистре его при помощи макроса stts о, который
на практике возвращает следующие ассемблерные инструкции:
movl %crO, %еах
orl $8,%еах
movl %еах, %сгО.
Загрузка регистров FPU
Содержимое регистров с плавающей точкой не восстанавливается сразу по-
после того, как возобновляется выполнение процесса next. Однако флаг ts в
регистре его был установлен макросом uniazyfpuo, поэтому, как только
процесс next попытается выполнить инструкцию ESCAPE, MMX или
SSE/SSE2, управляющий блок возбудит исключение "Устройство недоступ-
недоступно", а ядро (точнее, обработчик этого исключения) вызовет функцию
mathstaterestore (). В этом обработчике процесс next идентифицируется
как current.
void math_state_restore ()
{
asm volatile ("cits"); /* сбросить флаг TS в регистре сгО */
if (!(current->flags & PF_USED_MATH))
init_fpu(current);
restore_fpu(current);
current->thread.status |= TSJJSEDFPU;
}
Функция сбрасывает флаг ts в регистре его, чтобы последующие инструкции
FPU, ММХ или SSE/SSE2, выполняемые процессом, не привели к возникно-
возникновению исключения "Устройство недоступно". Если содержимое субполя
thread. i387 смысла не имеет, т.е. флаг pfusedmath равен 0, вызывается
функция initfpuO, которая сбрасывает субполе thread. i387 и устанавли-
устанавливает флаг pfusedmath процесса current. Затем вызывается функция
restorefpu(), загружающая в регистры FPU соответствующие значения,
хранящиеся в субполе thread. i387. Для этого применяется ассемблерная ин-
инструкция fxrstor или f rstor, в зависимости от того, поддерживает ли процес-
процессор расширения SSE/SSE2. Наконец, функция mathstaterestore () устанав-
устанавливает флаг tsjjsedfpu.
Использование блоков FPU, ММХ и SSE/SSE2
в режиме ядра
Ядро само может использовать блоки FPU, ММХ и SSE/SSE2. Конечно, при
этом оно не должно мешать вычислениям, которые выполняет текущий про-
процесс в пользовательском режиме. Поэтому
□ перед использованием сопроцессора ядро должно вызвать функцию
kernelf pubegin (), КОТОрая вызывает функцию saveinitfpu (), чтобы
сохранить содержимое регистров, если процесс в пользовательском режи-
режиме выполнял инструкции FPU (флаг tsusedfpu), а затем сбрасывает флаг
ts в регистре его;
□ после использования сопроцессора ядро должно вызвать функцию
kerneifpuend (), которая устанавливает флаг ts в регистре его.
Впоследствии, когда процесс пользовательского режима выполнит инструк-
инструкцию сопроцессора, функция mathstaterestoreo восстановит содержимое
регистров, аналогично тому, как это происходит при переключении про-
процессов.
Следует ОТМетИТЬ, ЧТО Время работы фуНКЦИИ kernelf pubegin () ДОВОЛЬНО
велико, если текущий процесс пользовательского режима обращается к со-
сопроцессору. Настолько велико, что этого достаточно для аннулирования вы-
выигрыша, полученного от применения блоков FPU, ММХ или SSE/SSE2. На
практике ядро пользуется ими лишь в некоторых ситуациях, как правило, при
переносе или очистке больших областей памяти или при вычислении кон-
контрольных сумм.
Создание процессов
При удовлетворении запросов пользователей Unix-подобные операционные
системы активно создают новые процессы. Например, всякий раз, когда
пользователь вводит команду, оболочка создает новый процесс, который вы-
выполняет еще одну копию оболочки.
Традиционные Unix-системы обращаются со всеми процессами одинаково:
ресурсы, которыми обладает процесс-родитель, дублируются в процессах-
потомках. Такой подход делает процедуру создания процессов очень медлен-
медленной и неэффективной, потому что включает в себя копирование всего адрес-
адресного пространства процесса-родителя. Процессу-потомку редко нужно читать
или модифицировать все ресурсы, унаследованные от родителя; в большин-
большинстве случаев он тут же делает системный вызов execve () и очищает столь за-
заботливо скопированное адресное пространство.
Современные ядра Unix-систем решают эту проблему с помощью разных ме-
механизмов:
□ техника копирования при записи (Copy On Write, COW) позволяет как ро-
родителю, так и потомку читать одни и те же физические страницы. Когда
один из этих процессов попытается сделать запись в физическую страни-
страницу, ядро копирует ее содержимое в новую физическую страницу, которая
присваивается пишущему процессу. Реализация этой техники в Linux под-
подробно описана в главе 9;
□ облегченные процессы позволяют как родителю, так и потомку совместно
использовать многие структуры данных ядра, назначаемые процессам.
Сюда входят таблицы страниц (и, следовательно, все адресное простран-
пространство режима пользователя, таблицы открытых файлов и диспозиций сиг-
сигналов);
□ системный вызов vforko создает процесс, который использует адресное
пространство своего родителя. Чтобы не позволить родителю переписать
данные, необходимые потомку, выполнение родителя задерживается до
тех пор, пока потомок не закончит свою работу или не запустит новую
программу. Системный вызов vfork () более подробно обсуждается в сле-
следующем разделе.
Системные вызовы clone(), forkf) и vforkQ
Облегченные процессы создаются в Linux с помощью функции clone (), кото-
которая принимает следующие параметры:
□ fn — задает функцию, которую должен выполнить новый процесс; когда
эта функция возвратит управление, потомок завершит свою работу. Функ-
ция возвращает целое число, представляющее собой код возврата процес-
процесса-потомка;
□ arg — указывает на данные, необходимые для функции f n ();
□ flags — разнообразная информация. Младший байт задает номер сигнала,
посылаемого родителю, когда потомок завершит работу (как правило, вы-
выбирается сигнал sigchld). Остальные три байта задают группу флагов кло-
клона, перечисленных в табл. 3.8;
□ chiidstack— задает указатель стека в режиме пользователя, который
должен быть записан в регистр esp процесса-потомка. Вызывающий про-
процесс (родитель) должен всегда выделять стек для нового потомка;
□ tis — задает адрес структуры, определяющей сегмент локальной памяти
потока для нового облегченного процесса (см. главу 2). Имеет смысл толь-
только при установленном флаге clonesettls;
□ ptid — задает адрес переменной процесса-родителя в режиме пользовате-
пользователя, где будет храниться идентификатор нового облегченного процесса.
Имеет смысл только при установленном флаге cloneparentsettid;
П ctid— задает адрес переменной нового облегченного процесса в режиме
пользователя, где будет храниться идентификатор этого процесса. Имеет
смысл только при установленном флаге clonechildsettid.
Таблица 3.8. Флаги клона
Имя флага Описание
clonevm Совместно использует дескриптор памяти и все таблицы
страниц (см. главу 9)
clonefs Совместно использует таблицу, которая идентифицирует
корневой каталог и текущий рабочий каталог, а также бито-
битовую маску, задающую права доступа для вновь создаваемых
файлов (так называемое значение umask)
clone_files Совместно использует таблицу, которая идентифицирует
открытые файлы (см. главу 12)
clone_sighand Совместно использует таблицы, которые идентифицируют
обработчики сигналов, а также задержанные и еще не дос-
доставленные сигналы (см. главу 11). Если этот флаг установ-
установлен, флаг С1ШЕ_умтоже должен быть установлен
cloneptrace Если ведется мониторинг, то родитель хочет, чтобы проис-
происходил и мониторинг потомка. Кроме того, отладчик может
самостоятельно захотеть отслеживать работу потомка и в
этом случае ядро принудительно устанавливает флаг в 1
clonevfork Флаг устанавливается при выдаче системного вызова
vfork()
Таблица 3.8 (окончание)
Имя флага Описание
clone_parent Приравнивает родителя потомка (поля parent и real_
parent в дескрипторе процесса) к родителю вызвавшего
процесса
clone_thread Заносит процесс-потомок в группу потоков, в которую входит
родитель, и заставляет процесс-потомок использовать деск-
дескриптор сигнала совместно с родителем. Соответствующим
образом устанавливаются поля tgid и group_leader потом-
потомка. Если этот флаг установлен, флаг clone_sighand тоже
должен быть установлен
clone_newns Флаг установлен, если клон нуждается в собственном про-
пространстве имен, т. е. собственный вид на смонтированные
файловые системы (см. главу 12). Установить одновременно
флаги CLONE_NEWNS И CLONE__FS НвВОЗМОЖНО
clone_sysvsem Совместно использует отменяемые семафорные операции в
System V IPC (см. разд. "Семафоры IPC" главы 19)
clone_settls Создает новый сегмент локальной памяти потока для облег-
облегченного процесса. Этот сегмент описан в структуре, на кото-
которую указывает параметр tls
clone_parent_settid Записывает идентификатор процесса-потомка в переменную
режима пользователя родителя, на которую указывает па-
параметр ptid
clone_child_cleartid Когда флаг установлен, ядро настраивает механизм, кото-
который сработает, когда процесс-потомок завершит свое вы-
выполнение или запустит новую программу. В этих случаях
ядро очистит переменную пользовательского режима, на
которую указывает параметр ctid, и разбудит любой про-
процесс, ожидающий это событие
clone_detached Флаг, унаследованный от предыдущих версий и игнорируе-
игнорируемый ядром
clonejjntraced Устанавливается ядром для переопределения значения
флага clone_ptrace (используется для отмены мониторинга
потоков ядра; см. разд. "Потоки ядра" далее в этой главе)
clone_child_settid Записывает идентификатор процесса-потомка в переменную
потомка пользовательского режима, на которую указывает
параметр ctid
clone_stopped Заставляет процесс-потомок стартовать в состоянии
TASK_STOPPED
Функция clone о фактически является интерфейсной функцией, определен-
определенной в библиотеке С (см. разд. "API-интерфейсы стандарта POSIX и сис-
системные вызовы4 в главе 10). Она устанавливает стек для нового облегченного
процесса и делает системный вызов clone о, скрытый от программиста.
У служебной процедуры syscione (), реализующей системный вызов clone (),
нет параметров fn и arg. Интерфейсная функция сохраняет указатель fn в
стеке потомка в позиции, соответствующей адресу возврата самой интер-
интерфейсной функции, а указатель arg сохраняется в стеке потомка сразу под fn.
Когда интерфейсная функция завершается, процессор извлекает адрес воз-
возврата ИЗ Стека И ВЫПОЛНЯеТ фуНКЦИЮ fn (arg).
Традиционный системный вызов fork () реализован в Linux в виде системно-
системного вызова clone о, у которого параметр flags задает сигнал sigchld и опреде-
определяет, что все флаги клона сброшены, а параметр chiidstack является указа-
указателем на текущий стек процесса-родителя. Таким образом, родитель и пото-
потомок временно совместно используются одним стеком режима пользователя.
Однако, благодаря механизму копирования при записи, они обычно получают
отдельные копии стека режима пользователя, как только один из них попыта-
попытается модифицировать стек.
Системный вызов vfork (), упомянутый в предыдущем разделе, реализован в
Linux в виде системного вызова clone о, у которого параметр flags задает
сигнал sigchld и флаги clone_vm и clone_vfork, а параметр chiid_stack явля-
является указателем на текущий стек процесса-родителя.
Функция doJorkQ
Функция doforko, обрабатывающая системные вызовы clone о, fork о и
vf ork (), принимает следующие параметры:
□ clone_flags — ТО же, ЧТО параметр flags функции clone ();
□ stack_start — ТО же, ЧТО параметр child_stack функции clone ();
□ regs — указатель на содержимое регистров общего назначения, сохранен-
сохраненное в стеке режима ядра при переключении из пользовательского режима
в режим ядра (см. разд. "Функция doIRQQ " главы 4);
□ stacksize — не используется (всегда равен 0);
□ parent_tidptr, child_tidptr— ТО же, ЧТО параметры ptid И ctid функции
clone().
Функция doforko вызывает служебную функцию copyprocess о, чтобы ус-
установить дескриптор процесса и прочие структуры данных ядра, необходи-
необходимые для работы потомка. Приведем основные действия, выполняемые функ-
функцией do_fork():
1. Выделяет идентификатор процесса для потомка, просматривая битовую
карту pidmap_array.
2. Проверяет поле ptrace процесса-родителя (то есть поле current->ptrace).
Если оно не равно нулю, значит, родитель отслеживается другим процес-
процессом, и тогда функция do_fork () проверяет, хочет ли отладчик отслеживать
процесс-потомок по своей инициативе (то есть независимо от флага
cloneptrace, задаваемого родителем). В этом случае, если потомок не яв-
является потоком ядра (флаг cloneuntraced сброшен), функция устанавли-
устанавливает флаг CLONE__PTRACE.
3. Вызывает функцию copyprocesso, чтобы создать копию дескриптора
процесса. Если все необходимые ресурсы доступны, функция возвращает
адрес только что созданного дескриптора taskstruct. Эта функция —
"рабочая лошадка" процедуры ветвления процесса, и мы обсудим ее сразу
после функции dof ork ().
4. Если либо установлен флаг clonestopped, либо необходим мониторинг
процесса-потомка (то есть флаг рт ptraced в поле p->ptrace установлен),
функция переводит процесс-потомок в состояние taskstopped и добавляет
к нему сигнал sigstop, подлежащий обработке (см. разд. "Роль сигналов"
главы 11). Потомок остается в состоянии taskstopped, пока какой-то дру-
другой процесс (предположительно, отслеживающий выполнение или роди-
родитель) не переведет его в состояние taskrunning с помощью сигнала
SIGCONT.
5. Если флаг clonestopped не установлен, функция вызывает функцию
wakeupnewtask (), которая выполняет следующие действия:
• настраивает параметры планирования родителя и потомка (см.
разд. "Алгоритм планирования'1 главы 7);
• если потомок будет выполняться на том же процессоре, что и роди-
родитель8, и родитель с потомком не обращаются к одному набору таблиц
страниц (флаг clonevm сброшен), функция заставляет процесс-потомок
выполняться до родителя, занося процесс-потомок в очередь на выпол-
выполнение, в которой стоит родитель, непосредственно перед родителем.
Это несложное действие повышает производительность в том случае,
когда потомок очищает свое адресное пространство и запускает новую
программу сразу после ветвления. Если бы мы позволили родителю
выполняться первым, механизм копирования при записи произвел бы
бесполезную дубликацию страниц;
• в противном случае, если потомку не предстоит работать на одном
процессоре с родителем, или если родитель с потомком совместно ис-
используют один набор таблиц страниц (флаг clonevm установлен),
8 Пока ядро выполняло ветвление, процесс-родитель мог быть перенесен на другой процессор.
функция ставит процесс-потомок на последнее месте родительской
очереди на выполнение.
6. Если флаг clonestopped установлен, функция переводит процесс-потомок
в состояние task_stopped.
7. Если выполнение процесса-родителя отслеживается, функция сохраняет
идентификатор процесса-потомка в поле ptracemessage текущего процес-
процесса и вызывает функцию ptracenotifyO, которая фактически останавлива-
останавливает текущий процесс и отправляет сигнал sigchld его родителю. "Дедуш-
"Дедушкой" процесса-потомка здесь является отладчик, отслеживающий выпол-
выполнение родителя. Сигнал sigchld уведомляет отладчик, что текущий
процесс породил процесс, чей идентификатор может быть получен из поля
current->ptrace_message.
8. Если установлен clonevfork, функция ставит процесс-родитель в очередь
ожидания и приостанавливает его выполнение до тех пор, пока потомок не
освободит адресное пространство (то есть пока он не завершит работу или
не запустит новую программу).
9. Завершает работу и возвращает идентификатор процесса-потомка.
Функция copy_process()
Функция copyprocess о устанавливает дескриптор процесса и любые другие
структуры данных ядра, которые могут потребоваться для выполнения по-
потомка. Ее параметры такие же, как у функции do_f ork (), плюс идентификатор
процесса-потомка. Она выполняет следующие действия:
1. Проверяет, совместимы ли флаги, переданные в параметре clonefiags.
В частности, она возвращает код ошибки в следующих случаях:
• установлены флаги clone_newns и clone_fs;
• флаг clone_thread установлен, но флаг clone_sighand сброшен (облег-
(облегченные процессы в одной группе потоков должны иметь общие сиг-
сигналы);
• флаг clonesighand установлен, но флаг clonevm сброшен (облегченные
процессы, имеющие общие обработчики сигналов, должны иметь и
общий дескриптор памяти).
2. Выполняет дальнейшие защитные проверки, для чего вызывает функцию
security_task_create() И, чуть ПОЗЖе, функцию security_task_alloc ().
Ядро Linux 2.6 предлагает перехватчики для расширений безопасности,
чтобы установить модель безопасности более строгую, чем та, что принята
в традиционных Unix-подобных системах (см. главу 20).
3. Вызывает функцию duptaskstructo, чтобы получить дескриптор про-
процесса для потомка. Эта функция выполняет следующие действия:
• вызывает функцию uniazy_fpu() для текущего процесса, чтобы со-
сохранить, если необходимо, содержимое регистров FPU, ММХ и
SSE/SSE2 в структуре threadinfo процесса-родителя. Впоследствии
функция duptaskstructо скопирует эти значения в структуру
threadinf о процесса-потомка;
• выполняет макрос aiioctaskstructо, чтобы получить дескриптор
(структуру taskstruct) для нового процесса, и сохраняет его адрес в
локальной переменной tsk;
• выполняет макрос aiiocthreadinfo, чтобы получить свободную об-
область памяти для хранения структуры threadinf о и стека режима ядра
для нового процесса, а затем сохраняет ее адрес в локальной перемен-
переменной ti. Как было сказано в разд. "Идентификация процесса" ранее
в этой главе, размер этой области памяти равен либо 8 Кбайт, либо
4 Кбайт;
• копирует содержимое дескриптора текущего процесса в структуру
taskstruct, на которую указывает локальная переменная tsk, затем за-
записывает значение ti В ПОЛе tsk->thread_infо;
• копирует содержимое дескриптора threadinfo текущего процесса в
структуру, на которую указывает локальная переменная ti, а затем за-
записывает значение tsk в поле ti->task;
• записывает 2 в счетчик обращений дескриптора нового процесса
(tsk->usage), чтобы показать, что дескриптор процесса используется и
что соответствующий процесс "жив" (его состояние не exitzombie и не
exit_dead);
• возвращает указатель на дескриптор нового процесса (tsk).
4. Проверяет Значение, Хранящееся В ПОЛе current->signal->rlim[RLIMIT_
NPROC].riim_cur. Если оно меньше или равно текущему количеству про-
процессов, принадлежащих пользователю, возвращается код ошибки, если у
процесса нет привилегий root. Функция получает текущее количество
процессов, принадлежащих пользователю, из пользовательской структуры
по имени userstruct. Эту структуру можно найти по указателю в поле
user дескриптора процесса.
5. Увеличивает счетчик обращений структуры userstruct (поле tsk->
user-> count) и счетчик процессов, принадлежащих пользователю (поле
tsk->user->processes).
6. Убеждается, что количество процессов в системе (хранящееся в перемен-
переменной nrthreads) не превышает значение переменной maxthreads. Значе-
Значение этой переменной, принимаемое по умолчанию, зависит от объема
оперативной памяти в системе. Общее правило гласит, что память, зани-
занимаемая всеми дескрипторами threadinf о и стеками режима ядра, не мо-
может превышать 1/8 объема физической памяти. Впрочем, системный ад-
администратор может изменить это значение, записав новое в файл
/proc/sys/kernel/threads-max.
7. Если функции ядра, реализующие область выполнения и поддержку ис-
исполняемого формата (см. главу 20) процесса, входят в состав модулей яд-
ядра, описываемая функция увеличивает их счетчики обращений (см. при-
приложение 2).
8. Устанавливает несколько важных полей, имеющих отношение к состоя-
состоянию процесса:
• инициализирует счетчик глобальной блокировки ядра tsk->iock_depth
значением-1 (см. разд. "Глобальная блокировка ядра" главы 5);
• инициализирует поле tsk->did_exec значением 0. Это счетчик систем-
системных вызовов execve (), сделанных процессом;
• обновляет некоторые флаги в поле tsk->f lags, скопированные у роди-
родителя: вначале сбрасывает флаг pfsuperpriv, показывающий, восполь-
воспользовался ли процесс какими-либо привилегиями суперпользователя, а
затем устанавливает флаг pfforknoexec, показывающий, что потомок
еще не сделал системный вызов execve ().
9. Сохраняет идентификатор нового процесса в поле tsk->pid.
10. Если флаг clone_parent_settid в параметре cione_fiags установлен,
функция копирует идентификатор процесса-потомка в переменную поль-
пользовательского режима, на которую указывает параметр parenttidptr.
11. Инициализирует структуры listhead и спин-блокировки, входящие в со-
состав дескриптора процесса-потомка, и устанавливает некоторые другие
поля, имеющие отношение к ожидающим доставки сигналам, таймерам и
хронометрической статистики.
12. Вызывает функции copy_semundo (), copy_files (), copy_fs(), copy_
sighand (), copy_signal (), copy__mm() И copy_namespace (), чтобы создать НО-
НОВЬЮ структуры и занести в них значения соответствующих структур ро-
родителя, если противоположное не указано с помощью параметра
clone_flags.
13. Вызывает функцию copythread (), чтобы инициализировать стек режима
ядра для процесса-потомка значениями, содержавшимися в регистрах
процессора в момент выдачи системного вызова clone о (эти значения
были сохранены в родительском стеке режима ядра; см. главу 10). При
этом, однако, функция принудительно записывает 0 в поле, соответст-
соответствующее регистру еах (это код возврата системного вызова fork о или
clone о в процессе-потомке). Поле thread, esp в дескрипторе потомка
инициализируется базовым адресом стека режима ядра процесса-
потомка, а адрес ассемблерной функции ret fromforko сохраняется в
поле thread.eip. Если родитель пользуется битовой картой разрешений
ввода/вывода, потомок получает копию этой карты. Наконец, если уста-
установлен флаг clonesettls, потомок получает TLS-сегмент, определяемый
структурой данных режима пользователя, на которую указывает параметр
tls СИСТеМНОГО ВЫЗОВа clone ()9.
14. Если установлен флаг clone_child_settid или clone_child_cleartid в па-
параметре clonef lags, фуНКЦИЯ копирует Значение параметра child_tidptr
В ПОЛе tsk->set_chid_tid ИЛИ tsk->clear_child_tid Соответственно. Эти
флаги говорят о том, что значение переменной, на которую указывает па-
параметр childtidptr, в адресном пространстве потомка в режиме пользо-
пользователя должно быть изменено, хотя физически операции записи будут
выполнены позже.
15. Сбрасывает флаг tifsyscalltrace в структуре threadinfo процесса-
потомка, чтобы функция retfromfork() не уведомляла процесс-
отладчик о завершении системного вызова (см. разд. "Вход в системный
вызов и выход из него" в главе 10). Заметим, что при этом системный вы-
вызов на мониторинг потомка не отменяется, потому что этим управляет
флаг PTRACE_SYSCALL В ПОЛе tsk->ptrace.
16. Инициализирует поле tsk->exit_signai номером сигнала, указанным в
младших битах параметра clonefiags, если флаг clonethread не уста-
установлен. В последнем случае инициализирует это поле значением -1. Как
мы увидим в разд. "Завершение процесса" далее в этой главе, только за-
завершение работы последнего члена группы потоков (как правило, это ли-
лидер группы) приводит к отправке уведомляющего сигнала родителю ли-
лидера группы.
9 Внимательный читатель, возможно, удивится, откуда функция copy_thread () возьмет значение
параметра tls системного вызова clone (), ведь tls не передается функции do_fork () и другим
вложенным функциям. Как мы увидим далее, параметры системных вызовов обычно передаются
ядру путем копирования их значений в некоторые регистры процессора, и, следовательно, эти зна-
значения сохраняются в стеке режима ядра вместе с остальными регистрами. Функция copy_thread ()
просто обращается по адресу, сохраненному в стеке режима ядра, в той его ячейке, которая соответ-
соответствует значению esi.
17. Вызывает функцию sched_fork (), чтобы завершить инициализацию
структуры данных для планировщика, принадлежащей новому процессу.
Функция также переводит новый процесс в состояние taskrunning и за-
записывает 1 В ПОЛе preemptcount Структуры threadinfo, ОТКЛЮЧая тем
самым вытеснение в ядре (см. разд. "Вытеснение в ядре" в главе 5). Кро-
Кроме того, чтобы планирование процессов имело справедливый характер,
функция делит оставшийся отрезок времени родителя между родителем и
потомком (см. разд. "Функция scheduler_tick() " в главе 7).
18. Записывает в поле ери структуры threadinfo нового процесса номер ло-
локального Процессора, ПОЛучеННЫЙ ОТ фуНКЦИИ smp_processor_id ().
19. Инициализирует поля, определяющие отношения между родителем и по-
потомком. В частности, если флаг clone_parent или clone_thread установ-
установлен, функция инициализирует поля tsk->reai_parent и tsk->parent значе-
нием, хранящимся в поле current->reai_parent, и в результате родитель
потомка выступает в качестве родителя текущего процесса. В противном
случае функция записывает в эти поля значение current.
20. Если мониторинг потомка не требуется (флаг cloneptrace не установ-
установлен), функция записывает 0 в поле tsk->ptrace. Это поле содержит не-
несколько флагов, используемых при мониторинге одного процесса другим.
Таким образом, даже если текущий процесс отслеживается, потомок от-
отслеживаться не будет.
21. Выполняет макрос setlinks, чтобы занести дескриптор нового процесса
в список процессов.
22. Если мониторинг потомка требуется (флаг ptptraced в поле tsk->ptrace
установлен), функция записывает в поле current->parent значение поля
tsk->parent и заносит процесс-потомок в список отслеживаемых процес-
процессов, принадлежащий отладчику.
23. Вызывает функцию attachpid (), чтобы занести идентификатор нового
Процесса В Хеш-таблицу pidhash [PIDTYPE_PID].
24. Если потомок является лидером группы потоков (флаг clonethread сбро-
сброшен), функция:
• инициализирует поле tsk->tgid значением из поля tsk->pid;
• инициализирует поле tsk->group_ieader значением из поля tsk;
• трижды вызывает функцию attachpid (), чтобы занести идентифика-
идентификатор процесса-потомка в хеш-таблицы pidtypetgid, pidtypepgid и
PIDTYPE_SID.
25. В противном случае, т. е. если потомок принадлежит к группе потоков
своего родителя (флаг clonethread установлен), функция:
• Инициализирует ПОЛе tsk->tgid Значением ИЗ ПОЛЯ tsk->current->tgid;
• Инициализирует ПОЛе tsk->group_leader Значением ИЗ ПОЛЯ current->
group_leader*
• вызывает функцию attachpid(), чтобы занести процесс-потомок в
хеш-таблицу pidtypetgid (точнее, в список, соответствующий иден-
идентификатору процесса и принадлежащий процессу current->group_
leader).
26. Итак, новый процесс добавлен к множеству существующих процессов.
Функция увеличивает значение переменной nrthreads.
27. Устанавливает значение переменной totaiforks, чтобы отразить в ней
количество ответвленных процессов.
28. Завершает выполнение, возвращая указатель на дескриптор процесса-
потомка (tsk).
Обсудим, что происходит после завершения функции do_fork (). Теперь мы
имеем полноценный процесс-потомок в состоянии "выполняемый". Однако
фактически он не выполняется. Принятие решения о передаче его процессору
остается за планировщиком. В будущем, при очередном переключении про-
процессов, планировщик окажет нашему процессу такую любезность, загрузив в
соответствующие регистры процессора значения из поля thread дескриптора
этого процесса. В частности, в регистр esp будет загружено значение
thread.esp (то есть адрес стека процесса-потомка в режиме ядра), а в регистр
eip — адрес функции retf romf ork (). Эта функция, написанная на ассемб-
ассемблере, вызывает функцию scheduietaiio (которая, в свою очередь, вызывает
функцию finishtaskswitcho, чтобы закончить переключение процессов;
см. разд. "Функция schedule()" главы 7), загружает в остальные регистры зна-
значения, хранящиеся в стеке, и переводит процессор в режим пользователя, вы-
выполнение нового процесса начнется сразу по окончании работы системного
вызова fork (), vfork () или clone (). Значение, возвращенное системным вы-
вызовом, содержится в регистре еах: оно равно 0 для потомка и идентификатору
процесса для родителя. Чтобы понять, как это получается, вернитесь к описа-
описанию действий функции copythread() над регистром еах для процесса-
потомка. Потомок выполняет тот же код, что и родитель, с той разницей, что
системный вызов fork() возвращает 0 (шаг 13 в описании функции
copyprocess о). Разработчик приложения может воспользоваться этим фак-
фактом, применив прием, знакомый всем, кто программирует в Unix. Следует
поставить в программе условный оператор, проверяющий идентификатор
процесса и заставляющий процесс-потомок вести себя иначе, чем его роди-
родитель.
Потоки ядра
В традиционных Unix-системах некоторые критические задачи делегируются
перемежающимся процессам. Сюда входят сброс дисковых кэшей, выгрузка
неиспользуемых страниц, обслуживание сетевых соединений и другие зада-
задачи. В самом деле, выполнять эти задачи в строго линейной последовательно-
последовательности неэффективно; их функции и процессы конечного пользователя будут
иметь лучшее время отклика, если такие действия выполняются в фоновом
режиме. Поскольку некоторые системные процессы работают только в режи-
режиме ядра, современные операционные системы делегируют их функции
потокам ядра, не обремененным ненужным контекстом пользовательского
режима. В Linux потоки ядра отличаются от обычных процессов по следую-
следующим пунктам:
□ потоки ядра работают только в режиме ядра, а обычные процессы выпол-
выполняются попеременно то в режиме ядра, то в пользовательском режиме;
□ поскольку потоки ядра работают только в режиме ядра, они обращаются
только к линейным адресам, большим чем pageoffset; обычные же про-
процессы пользуются всеми четырьмя гигабайтами линейных адресов, будь то
в режиме пользователя или в режиме ядра.
Создание потока ядра
Функция kerneithread () создает новый поток ядра. Она принимает в качест-
качестве параметров адрес функции ядра, подлежащей выполнению (fn), аргумент,
передаваемый этот функции (arg), и набор флагов клона (flags). Фактически
она вызывает функцию do_f ork () со следующими аргументами:
do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, pregs, О, NULL, NULL);
Флаг clonevm позволяет избежать копирования таблиц страниц вызвавшего
процесса. Такое копирование было бы непроизводительной тратой времени и
памяти, потому что новый поток ядра все равно не станет обращаться к ад-
адресному пространству пользовательского режима. Флаг cloneuntraced га-
гарантирует, что никакой процесс не сможет отслеживать выполнение нового
потока ядра, даже если производилось отслеживание вызвавшего процесса.
Параметр pregs, передаваемый функции doforko, соответствует адресу в
стеке режима ядра, по которому функция copythread () сможет найти на-
начальные значения регистров процессора для нового потока. Функция
kerneithreadO строит эту стековую область так, чтобы выполнялось сле-
следующее:
П регистры ebx и edx получили от функции copythread () значения парамет-
параметров f n и arg соответственно;
□ регистр eip получил адрес следующего фрагмента ассемблерного кода:
movl %edx,%eax
pushl %edx
call *%ebx
pushl %eax
call do_exit
Таким образом, новый поток ядра начнется с выполнения функции fn(arg).
Если эта функция завершится, поток ядра сделает системный вызов exit (),
передав ему значение, возвращенное функцией f n () (см. разд. "Уничтожение
процессов" далее в этой главе).
Процесс О
Предок всех процессов, называемый процессом 0, или холостым процессом,
или (по историческим причинам) процессом swapper, представляет собой по-
поток ядра, созданный "с нуля" на этапе инициализации Linux (см. приложе-
приложение 1). Этот всеобщий предок использует следующие статически выделяемые
структуры (структуры для всех остальных процессов выделяются динами-
динамически):
□ дескриптор процесса, хранящийся в переменной inittask, которая ини-
инициализируется макросом inittask;
□ дескриптор threadinfo и стек режима ядра, хранящиеся в переменной
init_thread_union И Инициализируемые МаКрОСОМ INIT_THREAD_INFO;
□ таблицы, на которые указывает дескриптор процесса:
• init_inm
• init_fs
• init_files
• init signals
• init_sighand
Эти таблицы инициализируются, соответственно, следующими макросами:
• INIT_MM
• INIT_FS
• INIT_FILES
• INIT_SIGNALS
• INIT_SIGHAND
□ главный глобальный каталог страниц ядра, хранящийся в переменной
swapper_pg_dir (см. главу 2).
Функция startkernei () инициализирует все структуры данных, необходи-
необходимые ядру, включает прерывания и создает еще один поток ядра, процесс 1,
чаще называемый процессом init:
kernel_thread(init, NULL, CLONE_FS|CLONE_SIGHAND);
Этот новый поток ядра имеет идентификатор процесса, равный 1, и использу-
использует все соответствующие структуры данных ядра совместно с процессом 0.
Будучи выбранным планировщиком, процесс init запускает функцию init ().
Создав процесс init, процесс 0 вызывает функцию cpuidie (), которая, в сущ-
сущности, сводится к многократному выполнению ассемблерной инструкции hit
при включенных прерываниях (см. главу 4). Процесс 0 выбирается плани-
планировщиком, только если нет других процессов в состоянии taskrunning.
В многопроцессорных системах процесс 0 имеется у каждого процессора.
Сразу после включения питания система BIOS включает один процессор, ос-
оставив остальные выключенными. Процесс swapper, работающий на процессо-
процессоре 0, инициализирует структуры данных ядра, а затем включает остальные
процессоры и создает дополнительные процессы swapper с помощью функ-
функции copyprocessо, которой он передает ноль в качестве идентификатора
нового процесса. Кроме того, ядро записывает в поле ери дескриптора
threadinfo каждого ответвленного процесса соответствующий индекс про-
процессора.
Процесс 1
Поток ядра, созданный процессом 0, вызывает функцию init о, которая за-
завершает инициализацию ядра. Затем функция init () делает системный вызов
execve (), чтобы загрузить исполняемую программу init. В результате поток
ядра init становится обычным процессом, имеющим собственные структуры
данных ядра (см. главу 20). Процессор init продолжает существовать вплоть
до выключения питания, поскольку он создает и отслеживает все процессы,
реализующие внешние слои операционной системы.
Другие потоки ядра
Linux использует много других потоков ядра. Некоторые из них создаются на
этапе инициализации и работают до выключения системы; другие создаются
"по требованию", когда ядро должно выполнить задачу, для которой опти-
оптимальным является собственный контекст ядра.
Примерами потоков ядра (помимо процесса 0 и процесса 1) являются:
□ keventd (также называемый events) — выполняет функции из рабочей оче-
очереди keventd_wq (см. главу 4)\
□ kapmd— обрабатывает события, имеющие отношение к усовершенство-
усовершенствованному управлению питанием (АРМ, Advanced Power Management);
□ kswapd — утилизирует память, как описано в разд. "Периодическая ути-
утилизация " главы 17;
□ pdflush — сбрасывает "грязные" буферы на диск, чтобы утилизировать
память, как описано в разд. "Потоки ядра pdflush" главы 15;
□ kblockd ВЫПОЛНЯеТ фуНКЦИИ ИЗ рабочей ОЧереДИ kblockd_workqueue. На
практике этот поток ядра периодически активизирует драйверы блочных
устройств, как описано в разд. "Активизация драйвера блочного устрой-
устройства" главы 14;
П ksoftirqd— выполняет тасклеты (см. разд. "Softirq-функции и тасклеты"
в главе 4). У каждого процессора в системе есть один такой поток ядра.
Уничтожение процессов
Большинство процессов "умирает" в том смысле, что они прекращают вы-
выполнять свой код. Когда это происходит, ядро должно получить уведомление,
чтобы оно могло освободить ресурсы, принадлежащие процессу: память, от-
открытые файлы и прочие вещи, упоминаемые в этой книге, например, сема-
семафоры.
Обычно, чтобы закончить свою работу, процесс вызывает библиотечную
функцию exit (), которая освобождает ресурсы, выделенные библиотекой С,
выполняет все функции, зарегистрированные программистом, и под конец
делает системный вызов, удаляющий процесс из системы. Библиотечная
функция exit () может быть вызвана программистом явно. Кроме того, ком-
компилятор С всегда ставит вызов exit () сразу после последнего оператора
фуНКЦИИ main ().
В качестве альтернативы ядро может принудительно завершить выполнение
целой группы потоков. Как правило, это происходит, когда процесс в группе
получает сигнал, который он не может ни обработать, ни игнорировать (см.
главу 11), или когда в режиме ядра возникает неустранимое исключение, в то
время как ядро работает от имени процесса (см. главу 4).
Завершение процесса
В Linux 2.6 существуют два системных вызова, которые прекращают выпол-
выполнение приложения, работающего в пользовательском режиме:
□ системный вызов exitgroup (), который завершает выполнение всей груп-
группы потоков, т. е. целое многопоточное приложение. Основная функция
ядра, реализующая этот системный вызов, называется dogroupexit().
Этот системный вызов должен быть сделан библиотечной функцией
exit ();
□ системный вызов exit (), который завершает выполнение одного процес-
процесса, независимо от других процессов, входящих в ту же группу, что и жерт-
жертва. Основная функция ядра, реализующая этот системный вызов, называ-
называется doexit(). Этот системный вызов делается, например, функцией
pthreadexit () из библиотеки LinuxThreads.
Функция do_group_exit()
Функция dogroupexit () уничтожает все процессы, принадлежащие к группе
потоков процесса current. Она принимает в качестве параметра код заверше-
завершения процесса, который представляет собой либо значение, указанное в сис-
системном вызове exitgroup () (нормальное завершение), либо код ошибки, пе-
переданный ядром (аварийное завершение). Функция выполняет следующие
действия:
1. Проверяет, установлен ли флаг signalgroupexit у завершающегося про-
процесса, т. е. приступило ли уже ядро к процедуре завершения данной груп-
группы потоков. В этом случае функция считает кодом завершения значение,
хранящееся в поле current->signai->group_exit_code, и переходит к
шагу 4.
2. В противном случае функция устанавливает у процесса флаг signal_
groupexit и сохраняет код завершения в поле current->signai->
group_exit_code.
3. Вызывает функцию zapotherthreads(), чтобы уничтожить остальные
процессы в группе процесса current, если таковые имеются. С этой целью
функция перебирает список идентификаторов процессов в хеш-таблице
pidtype_tgid, определяемой полем current->tgid. Каждому процессу в
списке, отличному от current, она посылает сигнал sigkill (cm. главу 11).
В результате, все эти процессы, в конце концов, выполнят функцию
doexit () и будут уничтожены.
4. Вызывает функцию doexit (), передавая ей код завершения процесса. Как
мы скоро увидим, функция doexit () уничтожает процесс и не возвращает
управление.
Функция do_exit()
Любое завершение процесса выполняется функцией doexit (), которая уда-
удаляет большинство ссылок на завершающийся процесс из структур данных
ядра.
Функция doexit () принимает в качестве параметра код завершения процесса
и выполняет следующие действия:
1. Устанавливает флаг pfjexiting в поле flag дескриптора процесса, чтобы
отметить тот факт, что процесс в данный момент удаляется.
2. Удаляет, если необходимо, дескриптор процесса из очереди динамическо-
динамического таймера, для чего вызывает функцию deitimersync () (см. главу 6).
3. Отсоединяет от дескриптора процесса структуры, имеющие отношение к
выделению страниц, семафорам, файловой системе, дескрипторам откры-
открытых файлов, пространствам имен и битовой карте разрешений вво-
ввода/вывода, ВЫЗЫВая С ЭТОЙ цеЛЬЮ функции exit_mm(), exit_sem(),
exit_f iles (), exit_f s (), exit_namespace () И exit_thread () COOTBeTCT-
венно. Эти же функции удаляют перечисленные структуры, если другие
процессы не пользуются ими совместно с уничтожаемым процессом.
4. Если функции ядра, реализующие область выполнения и поддержку ис-
исполняемого формата (см. главу 20) уничтожаемого процесса, входят в со-
состав модулей ядра, функция уменьшает их счетчики обращений.
5. Записывает код завершения процесса в поле exitcode дескриптора про-
процесса. Это значение представляет собой либо параметр системного вызова
exit о или exitgroupo (нормальное завершение), либо код ошибки,
предоставленный ядром (аварийное завершение).
6. Вызывает функцию exitnotifyO, которая выполняет следующие дей-
действия:
• обновляет отношения "родитель-потомок" между соответствующими
процессами. Все потомки, созданные уничтожаемым процессом, стано-
становятся потомками другого процесса в той же группе потоков, если тако-
таковой в данный момент выполняется. В противном случае они становятся
потомками процесса init;
• убеждается, что поле exitsignai дескриптора уничтожаемого процесса
отлично от-1, и что этот процесс является последним членом своей
группы потоков (заметим, что эти условия всегда соблюдены у любого
нормального процесса; см. шаг 16 в описании функции copyprocesso
ранее в этой главе). В этом случае функция посылает сигнал (обычно
sigchld) родителю уничтожаемого процесса, чтобы известить его о "ги-
"гибели" потомка;
• в противном случае, т. е. если поле exitsignai содержит -1, или груп-
группа потоков включает в себя другие процессы, функция отправляет сиг-
сигнал sigchld родителю только в том случае, когда процесс отслеживает-
отслеживается (в этой ситуации родителем является отладчик, который информиру-
информируется о "гибели" облегченного процесса);
• если поле exitsignai дескриптора процесса равно -1, а процесс не от-
отслеживается, функция записывает в поле exitstate дескриптора про-
процесса значение exitjdead и вызывает функцию reieasetasko для ути-
утилизации памяти, занимаемой оставшимися структурами данных про-
процесса, и уменьшения счетчика обращений дескриптора процесса (см.
следующий раздел). Счетчик обращений становится равным единице
(см. шагЗ функции copyprocess о), так что сам дескриптор процесса
пока не удаляется;
• если же поле exitsignai дескриптора процесса не равно -1, а процесс
отслеживается, функция записывает в поле exitstate значение
exitzombie. В следующем разделе мы увидим, что происходит с про-
процессами-зомби;
• устанавливает флаг pfdead в поле flags дескриптора процесса (см.
разд. "Функция schedule () " в главе 7).
7. Вызывает функцию schedule о (см. главу 7), чтобы выбрать, какой процесс
будет выполняться следующим. Поскольку процесс в состоянии
exitzombie игнорируется планировщиком, выполнение процесса прекра-
прекращается сразу после вызова макроса switchto в функции schedule о. Как
мы увидим в главе 7, планировщик проверит флаг pfdead и уменьшит
счетчик обращений в дескрипторе процесса-зомби, замещаемого другим
процессом, чтобы отметить тот факт, что процесс больше не существует.
Удаление процессов
Операционная система Unix позволяет процессу опрашивать ядро на предмет
идентификатора процесса родителя или состояния любого своего потомка.
Например, процесс может создать процесс-потомок для решения конкретной
задачи, а затем вызвать какую-нибудь wait о -подобную библиотечную функ-
функцию, чтобы проверить, завершилось ли выполнение потомка. Если заверши-
завершилось, код завершения позволит родителю судить об успешности решения за-
задачи.
Чтобы не возникло противоречий с такими возможностями, ядрам Unix не
разрешается уничтожать данные, содержащиеся в дескрипторе процесса, сра-
сразу после завершения процесса. Им можно сделать это только после того, как
процесс-родитель сделает wait о-подобный системный вызов в отношении
завершившегося процесса. Именно поэтому было введено состояние
exitzombie: хотя в техническом смысле процесса больше нет, его дескриптор
хранится, пока процесс-родитель не будет уведомлен.
Что происходит, если родитель завершает выполнение раньше своих потом-
потомков? В этом случае система может оказаться переполненной процессами-
зомби, дескрипторы которых навсегда остались бы в оперативной памяти.
Как было сказано ранее, эта проблема решается принудительным переводом
всех "осиротевших" процессов в потомки процесса init. Тогда процесс init
уничтожит зомби при проверке завершения одного из своих законных потом-
потомков с помощью wait () -подобного системного вызова.
Функция reieasetasko отсоединяет последние структуры от дескриптора
процесса-зомби. Она применяется к зомби одним из двух способов: либо с
помощью функции doexit (), если родитель не заинтересован в получении
СИГНаЛОВ ОТ ПОТОМКа, Либо С ПОМОЩЬЮ СИСТеМНЫХ ВЫЗОВОВ wait4() ИЛИ
waitpid (), после того как сигнал был отправлен потомку. В последнем случае
функция также произведет утилизацию памяти, занятой дескриптором про-
процесса, а в первом — утилизация памяти будет произведена планировщиком
(см. главу 7). Эта функция выполняет следующие действия:
1. Уменьшает количество процессов, принадлежащих пользователю-вла-
пользователю-владельцу завершившегося процесса. Это значение хранится в структуре
userstruct, упомянутой ранее в этой главе (см. шаг 4 функции сору_
process ()).
2. Если процесс отслеживается, функция удаляет его из списка ptrace_
children отладчика и возвращает оригинальному родителю.
3. Вызывает функцию exitsignai (), чтобы отменить все ожидающие дос-
доставки сигналы и освободить дескриптор signaistruct процесса. Если де-
дескриптор больше не используется другими облегченными процессами,
функция удаляет эту структуру. Кроме того, она вызывает функцию
exititimers (), чтобы отсоединить от процесса POSIX-таймер интервалов,
если таковой имеется.
4. Вызывает функцию exitsighando, чтобы убрать обработчики сиг-
сигналов.
5. Вызывает функцию unhashprocess (), которая:
• уменьшает переменную nrthreads на 1;
• дважды вызывает функцию detachpid(), чтобы удалить дескриптор
процесса из хеш-таблиц pidhash типа pidtype_pid и pidtype_tgid;
• если процесс является лидером группы, опять дважды вызывает функ-
функцию detachpid (), чтобы удалить дескриптор процесса из хеш-таблиц
PIDTYPE_PGID И PIDTYPE_SID;
• вызывает макрос removelinks, чтобы удалить дескриптор процесса из
списка процессов.
6. Если процесс не является лидером группы, значит, лидер уже стал процес-
процессом-зомби, и данный процесс является последним членом группы потоков.
Функция отправляет сигнал родителю лидера, чтобы уведомить его о "ги-
"гибели" процесса.
7. Вызывает функцию schedexit о, чтобы отрегулировать отрезок времени
процесса-родителя (этот шаг логически дополняет шаг 17 в описании
ФУНКЦИИ copy_process () ).
8. Вызывает функцию puttaskstruct о, чтобы изменить счетчик обраще-
обращений у дескриптора процесса. Если этот счетчик сравняется с нулем, функ-
функция отменяет все оставшиеся ссылки на процесс:
• уменьшает счетчик обращений (поле count) структуры userstruct,
принадлежащей пользователю-владельцу процесса (см. шаг 5 функции
copyprocess ()) и освобождает эту структуру, если счетчик обращений
становится равным 0;
• освобождает дескриптор процесса и область памяти, занимаемую деск-
дескриптором threadinf о и стеком режима ядра.
ГЛАВА 4
Прерывания и исключения
Прерывание обычно определяется как событие, меняющее последователь-
последовательность инструкций, выполняемых процессором. Такие события соответствуют
электрическим сигналам, генерируемым электронными схемами как внутри,
так и вне процессора.
Прерывания часто подразделяются на синхронные и асинхронные:
□ синхронные прерывания выдаются управляющим блоком процессора при
выполнении инструкций и называются синхронными, потому что управ-
управляющий блок процессора выдает их только по окончании выполнения ин-
инструкции;
□ асинхронные прерывания генерируются другими аппаратными устройст-
устройствами в произвольные моменты времени по отношению к тактовым сигна-
сигналам процессора.
В руководствах по микропроцессорам Intel синхронные и асинхронные пре-
прерывания обозначаются терминами "исключения" и "прерывания" соответст-
соответственно. Мы будем придерживаться этой классификации, хотя иногда мы бу-
будем говорить "сигнал прерывания", имея в виду сразу оба типа (синхронные и
асинхронные).
Прерывания генерируются таймерами интервалов и устройствами вво-
ввода/вывода. Например, когда пользователь нажимает на клавишу, возникает
прерывание.
Что касается исключений, они вызываются либо программными ошибками,
либо аномальными ситуациями, которые должны быть обработаны ядром.
В первом случае ядро обрабатывает исключение, доставляя текущему про-
процессу один из сигналов, хорошо известных любому программисту, работаю-
работающему в Unix. Во втором случае ядро выполняет действия, необходимые для
выхода из аномальной ситуации, такой как "ошибка обращения к странице"
или запрос на обслуживание ядром (посредством ассемблерной инструкции
int ИЛИ sysenter).
Мы начнем с того, что в следующем разделе изложим причины введения та-
таких сигналов. Затем мы покажем, как всем известные запросы IRQ (Interrupt
ReQuest, запрос на прерывание), выдаваемые устройствами ввода/вывода,
порождают прерывания, а также подробно опишем, как процессоры 80x86
обрабатывают прерывания и исключения на аппаратном уровне. После этого
в разд. "Инициализация таблицы дескрипторов прерываний" мы проиллюст-
проиллюстрируем, как Linux инициализирует все структуры данных, необходимые ар-
архитектуре прерываний 80x86. В оставшихся трех разделах описано, как Linux
обрабатывает сигналы прерываний на программном уровне.
Роль сигналов прерываний
Как следует из названия, сигналы прерываний предоставляют способ отвлечь
процессор от нормального выполнения программы на некоторый код, внеш-
внешний по отношению к ней. Когда поступает сигнал прерывания, процессор
должен прервать текущие дела и переключиться на другую деятельность. Для
этого он сохраняет текущее значение счетчика команд (то есть содержимое
регистров eip и cs) в стеке режима ядра и заносит в счетчик команд адрес,
определяемый типом прерывания.
В этой главе кое-что напомнит вам переключение контекста, описанное в
предыдущей главе, которое происходит, когда ядро замещает один процесс
другим. Однако между обработкой прерываний и переключением процессов
существует принципиальная разница: код, выполняемый обработчиком пре-
прерываний или исключений, не является процессом. Это управляющий тракт
ядра, который выполняется за счет процесса, работавшего в момент возник-
возникновения прерывания (см. разд. "Вложенное выполнение обработчиков исклю-
исключений и прерываний" далее в этой главе). Будучи частью ядра, обработчик
прерываний "легче" процесса (у него меньше контекст, и переключение на
него и с него занимает меньше времени).
Обработка прерываний является для ядра одной из задач, требующих исклю-
исключительно тонкого подхода, потому что она должна удовлетворять следующим
ограничениям:
□ Прерывания могут поступать в любой момент, например, когда ядро за-
заканчивает какую-то операцию. Следовательно, цель ядра в том, чтобы как
можно скорее "убрать прерывание с дороги" и отложить выполнение свя-
связанных с прерыванием действий, насколько это возможно. Например,
предположим, что по сети прибыл блок данных. Когда устройство прервет
ядро, последнее может просто отметить у себя наличие данных и позво-
позволить процессору доделать то, что он выполнял перед этим, а остальные
действия, касающиеся данных (копирование их в буфер процесса-полу-
процесса-получателя и запуск самого процесса), выполнить позже. Таким образом, дей-
действия, совершаемые ядром в ответ на прерывание, делятся на неотложные,
которые ядро выполняет сразу, и на те, которые можно оставить на потом.
□ Поскольку прерывания могут поступать в любой момент, возможна си-
ситуация, когда во время обработки одного прерывания возникает второе
(имеющее другой тип). Такой подход оправдан, потому что он позволяет
поддерживать занятость устройств ввода/вывода. Иными словами, обра-
обработчик прерываний должен быть написан так, чтобы соответствующие
части ядра могли быть выполнены вложенным образом. Когда завершится
последний обработчик, ядро должно быть в состоянии возобновить вы-
выполнение прерванного процесса или переключиться на другой процесс,
если сигнал прерывания вызвал активность планировщика.
□ Хотя ядро может принять новый сигнал прерывания во время обработки
предыдущего, в коде ядра имеются критические области, в которых пре-
прерывания должны быть отключены. Такие критические области должны
быть максимально ограничены, потому что в соответствии с предыдущим
требованием ядро и, в особенности, обработчики прерываний должны
большую часть времени работать при включенных прерываниях.
Прерывания и исключения
Документация Intel классифицирует прерывания и исключения следующим
образом.
Прерывания
Маскируемые прерывания — все запросы на прерывание (запросы IRQ), вы-
выдаваемые устройствами ввода/вывода, приводят к возникновению маскируе-
маскируемых прерываний. Маскируемое прерывание может иметь два состояния: "за-
"замаскировано" и "не замаскировано". Замаскированное прерывание игнориру-
игнорируется управляющим блоком все то время, пока оно таковым остается.
Немаскируемые прерывания — лишь немногие критические события (напри-
(например, аппаратные сбои) приводят к появлению немаскируемых прерываний.
Немаскируемые прерывания всегда принимаются процессором.
Исключения
Исключения, распознаваемые процессором — генерируются, когда процессор
распознает аномальную ситуацию во время выполнения инструкции. Эти ис-
ключения, в свою очередь, подразделяются на три группы, в зависимости от
значения регистра eip, которое сохраняется в стеке режима ядра, когда
управляющий блок процессора возбуждает исключение.
Ошибки— в общем случае могут быть исправлены; после их исправления
программе разрешается работать дальше без потери целостности. Сохранен-
Сохраненное значение регистра eip является адресом инструкции, вызвавшей ошибку,
и, следовательно, эта инструкция может быть выполнена снова, когда обра-
обработчик исключения возвратит управление. Как мы увидим в разд. "Обра-
"Обработчик исключения "ошибка обращения к странице'4' главы 9, возобновле-
возобновление выполнения той же инструкции необходимо, когда обработчик способен
исправить аномальную ситуацию, вызвавшую исключение.
Ловушки— срабатывают сразу после выполнения соответствующей инст-
инструкции. После того как ядро возвратит управление программе, ей разрешает-
разрешается работать дальше без потери целостности. Сохраненное значение регистра
eip является адресом инструкции, которая должна быть выполнена после ин-
инструкции, вызвавшей это исключение. Ловушка срабатывает, только когда
нет необходимости повторно выполнять уже выполненную инструкцию. Ло-
Ловушки применяются, в основном, для целей отладки. Роль сигнала прерыва-
прерывания в данном случае сводится к уведомлению отладчика о выполнении кон-
конкретной инструкции (например, в программе достигнута точка останова).
Изучив информацию, выданную отладчиком, пользователь может дать
команду на продолжение отлаживаемой программы, начиная со следующей
инструкции.
Аварии— произошла серьезная ошибка. Управляющий блок столкнулся с
проблемами и, возможно, не в состоянии записать в регистр eip адрес инст-
инструкции, вызвавшей исключение. Аварии применяются для индикации серьез-
серьезных ошибок, таких как сбои аппаратной части или противоречивые данные в
системных таблицах. Сигнал прерывания, посылаемый управляющим бло-
блоком, является тревожным сигналом, используемым для передачи управления
соответствующему обработчику исключений-аварий. У этого обработчика
нет иного выбора, кроме принудительного завершения проблемного про-
процесса.
Программные исключения — возникают по запросу программиста. Они вы-
вызываются инструкциями int или int3. Инструкции into (проверка на пере-
переполнение) и bound (проверка границы адреса) тоже могут возбудить про-
программное исключение, если проверяемое ими условие не истинно. Про-
Программные исключения обрабатываются управляющим блоком как ловушки, и
их число называют программными прерываниями. Такие исключения обычно
имеют два применения: для реализации системных вызовов и для уведомле-
уведомления отладчика о некотором событии (см. главу 10).
Каждое прерывание или исключение идентифицируется числом от 0 до 255.
Компания Intel называет это 8-битовое целое без знака вектором. Векторы
немаскируемых прерываний и исключений зафиксированы, а векторы маски-
маскируемых прерываний могут быть изменены путем программирования кон-
контроллера прерываний.
IRQ и прерывания
Любой контроллер аппаратного устройства, способный выдавать запросы на
прерывание, обычно имеет одну выходную линию, называемую IRQ-линией1.
Все IRQ-линии в системе соединены со входами электронной схемы, которая
называется программируемым контроллером прерываний. Она выполняет
следующие действия:
1. Ведет мониторинг IRQ-линий, проверяя наличие сигналов. Если возбуж-
возбуждены две или более IRQ-линий, выбирает ту, у которой меньше номер
входа.
2. Если на IRQ-линии возник сигнал:
• преобразует принятый сигнал в соответствующий вектор;
• сохраняет вектор в порте ввода/вывода контроллера прерываний, тем
самым позволяя процессору прочитать его через шину данных;
• посылает сигнал на вход INTR процессора, т. е. возбуждает прерыва-
прерывание;
• ждет, пока процессор подтвердит прием сигнала прерывания, записав
соответствующую информацию в один из портов ввода/вывода про-
программируемого контроллера прерываний. Когда это произойдет, сбра-
сбрасывает линию INTR.
3. Возвращается к шагу 1.
IRQ-линии перенумерованы, начиная с 0. Поэтому первая IRQ-линия обычно
обозначается IRQ 0. В компании Intel с линией IRQ n по умолчанию ассоции-
ассоциируется вектор п+32. Как было сказано ранее, соответствие между IRQ-
линиями и векторами может быть модифицировано с помощью соответст-
соответствующих инструкций ввода/вывода, подаваемых на порты контроллера пре-
прерываний.
Каждая IRQ-линия может быть избирательно отключена. Таким образом,
контроллер прерываний может быть запрограммирован на отключение за-
запросов IRQ. Иными словами, ему можно велеть прекратить выдачу прерыва-
прерываний, относящихся к данной IRQ-линии (или, наоборот, возобновить их выда-
1 У сложных устройств бывает несколько IRQ-линий. Например, у карты PCI может быть до четы-
четырех IRQ-линий.
чу). Отключенные прерывания не пропадают; программируемый контроллер
прерываний отправит их процессору, как только они будут включены. Эта
особенность используется в большинстве обработчиков прерываний, потому
что позволяет им обрабатывать IRQ-запросы одного типа последовательно.
Избирательное включение/отключение IRQ-линий — это не то же самое, что
глобальная маскировка/демаскировка маскируемых прерываний. Когда флаг
if в регистре efiags сброшен, любое маскируемое прерывание, выдаваемое
программируемым контроллером прерываний, временно игнорируется про-
процессором. Ассемблерные инструкции cli и sti соответственно сбрасывают и
устанавливают этот флаг.
Традиционные программируемые контроллеры прерываний реализуются
"каскадным" соединением двух внешних чипов типа 8259А. Каждый чип мо-
может работать максимум с восемью разными входными линиями IRQ. По-
Поскольку выходная INT-линия у подчиненного контроллера соединена со вхо-
входом IRQ 2 главного контроллера, количество доступных IRQ-линий ограни-
ограничено пятнадцатью.
Усовершенствованный программируемый
контроллер прерываний (APIC)
Приведенное описание относится к программируемым контроллерам преры-
прерываний, сконструированным для однопроцессорных систем. Если система
включает в себя единственный процессор, выходная линия главного контрол-
контроллера прерываний может быть напрямую соединена с INTR-входом процессо-
процессора. Однако если в системе имеется два и более процессоров, такое решение не
подходит, и возникает необходимость в более сложных контроллерах преры-
прерываний.
Чтобы в полной степени эксплуатировать параллелизм симметричной много-
многопроцессорной архитектуры, крайне важно иметь возможность доставлять
прерывания до любого процессора в системе. По этой причине, начиная с
процессора Pentium III, компания Intel ввела новый компонент, который
назвала I/O APIC (I/O Advanced Programmable Interrupt Controller, усовершен-
усовершенствованный программируемый контроллер прерываний). Этот чип представ-
представляет собой усовершенствованную версию старого программируемого кон-
контроллера прерываний 8259А. Для поддержки старых операционных систем
новейшие материнские платы включают в себя чипы обоих типов. Кроме то-
того, все современные процессоры 80x86 имеют локальный контроллер APIC.
Каждый локальный контроллер APIC содержит 32-разрядные регистры,
внутренние часы, локальный таймер и две дополнительные IRQ-линии,
LINT 0 и LINT 1, зарезервированные для прерываний локального контролле-
контроллера APIC. Все локальные контроллеры APIC соединены с внешним контрол-
контроллером I/O APIC, образуя многоконтроллерную APIC-систему.
На рис. 4.1 схематически изображена структура многоконтроллерной APIC-
системы. APIC-шина соединяет "фронтальный" контроллер I/O APIC с ло-
локальными контроллерами APIC. Линии IRQ, идущие от устройств, соединены
с контроллером I/O APIC, который, таким образом, выступает в качестве
маршрутизатора по отношению к локальным контроллерам. На материнских
платах Pentium III и более ранних процессоров шина APIC была последова-
последовательной трехлинейной шиной, а начиная с модели Pentium 4, шина APIC реа-
реализована на базе системной шины. Впрочем, поскольку шина APIC и ее со-
сообщения не видны программному обеспечению, мы не будем вдаваться в
дальнейшие подробности.
Рис. 4.1. Многоконтроллерная APIC-система
Контроллер I/O APIC включает в себя 24 линии IRQ, таблицу переадресации
прерываний на 24 записи, программируемые регистры и блок для отправки и
приема APIC-сообщений через APIC. В отличие от IRQ-выводов чипа 8259А,
приоритет прерывания не связан с номером линии. Каждая запись таблицы
переадресации может быть запрограммирована так, что она будет содержать
вектор и приоритет прерывания, целевой процессор и способ выбора процес-
процессора. Информация в таблице переадресации используется для трансляции
каждого внешнего IRQ-сигнала в сообщение, посылаемое одному или не-
нескольким локальным контроллерам APIC по шине APIC.
Запросы на прерывание, поступающие от внешних аппаратных устройств,
могут быть распределены между имеющимися процессорами одним из сле-
следующих способов:
□ статическое распределение— сигнал IRQ доставляется локальным кон-
контроллерам APIC, указанным в соответствующей записи таблицы переад-
ресации. Прерывание направляется одному конкретному процессору, под-
подмножеству процессоров или всем процессорам сразу (широковещательный
режим);
□ динамическое распределение — сигнал IRQ доставляется локальному
APIC-контроллеру процессора, выполняющему процесс с наименьшим
приоритетом.
У каждого локального контроллера APIC есть программируемый регистр
приоритета задачи (TPR), который используется при вычислении приорите-
приоритета текущего процесса. Предполагается, что этот регистр будет модифициро-
модифицироваться ядром операционной системы при каждом переключении процессов.
Если минимальный приоритет имеется у нескольких процессоров, нагрузка
распределяется между ними с помощью механизма, называемого арбитра-
арбитражем. Каждому процессору назначается свой арбитражный приоритет от О
(самый низкий) до 15 (наивысший), который хранится в регистре арбитраж-
арбитражных приоритетов локального контроллера APIC.
Всякий раз, когда процессору доставляется прерывание, его арбитражный
приоритет автоматически устанавливается равным 0, а арбитражные приори-
приоритеты всех остальных процессоров увеличиваются. Когда содержимое регист-
регистра арбитражных приоритетов превышает 15, в регистр записывается преды-
предыдущий арбитражный приоритет "выигравшего" процессора, увеличенный
на 1. Таким образом, прерывания распределяются между процессорами с
одинаковыми приоритетами задач по круговой системе2.
Вдобавок к распределению прерываний среди процессоров, многоконтрол-
многоконтроллерная APIC-система позволяет процессорам генерировать межпроцессорные
прерывания. Когда один процессор пытается отправить прерывание другому,
он сохраняет вектор прерывания и идентификатор APIC-контроллера целево-
целевого процессора в командном регистре прерываний (ICR) своего локального
контроллера APIC. Затем сообщение посылается по шине APIC локальному
APIC-контроллеру целевого процессора, и этот контроллер выдает соответст-
соответствующее прерывание своему процессору.
Межпроцессорные прерывания (IPI-прерывания) являются важнейшим эле-
элементом симметричной многопроцессорной архитектуры. Они активно ис-
используются операционной системой Linux для обмена сообщениями между
процессорами (см. далее в этой книге).
2 Локальный контроллер APIC процессора Pentium 4 не имеет регистра арбитражных приоритетов,
а механизм арбитража скрыт в электронной схеме арбитража шины. В документации Intel утвержда-
утверждается, что если ядро операционной системы не обновляет регистры приоритетов задач регулярно, то
производительность может оказаться ниже оптимальной, потому что не исключено, что прерывания
всегда будет обрабатывать один и тот же процессор.
Многие современные однопроцессорные системы имеют чип I/O APIC, кото-
который может быть сконфигурирован двумя различными способами:
□ как стандартный внешний программируемый контроллер прерываний
8259А, подключенный к процессору. Локальный контроллер APIC в этом
случае отключен, а локальные IRQ-линии LINT 0 и LINT 1 сконфигуриро-
сконфигурированы как выводы INTR и NMI соответственно;
□ как стандартный внешний контроллер I/O APIC. Локальный контроллер
APIC включен, и все внешние прерывания принимаются через контроллер
I/O APIC.
Исключения
Микропроцессоры 80x86 генерируют примерно 20 различных исключений3.
Ядро должно предоставить специальный обработчик для каждого типа ис-
исключения. При некоторых исключениях управляющий блок процессора гене-
генерирует также код аппаратной ошибки и помещает его в стек режима ядра до
запуска обработчика исключения.
В приведенном далее списке даны векторы, названия, типы и краткие описа-
описания исключений, генерируемых процессорами 80x86. Дополнительную ин-
информацию можно найти в технической документации Intel.
□ 0 — "Divide error" (Ошибка деления на ноль), ошибка. Возбуждается, ко-
когда программа пытается выполнить целочисленное деление на 0;
□ 1 — "Debug" (Отладка), ловушка или ошибка. Возбуждается, когда уста-
установлен флаг tf регистра efiags (очень полезна при пошаговом выполне-
выполнении отлаживаемой программы) или когда адрес инструкции или операнда
попадает в диапазон, установленный активным регистром отладки (см.
главу 3);
CU 2— не используется. Зарезервирован для немаскируемых прерываний
(тех, которые используют вывод NMI);
□ 3 — "Breakpoint" (Точка останова), ловушка. Вызывается с помощью ин-
инструкции int3 (точка останова), обычно вставляемой отладчиком;
П 4 — "Overflow" (Переполнение), ловушка. Инструкция into (проверка на
переполнение) была выполнена при установленном флаге of (переполне-
(переполнение) в регистре efiags;
□ 5 — "Bounds check" (Сбой проверки границ), ошибка. Инструкция bound
(проверка границы адреса) была выполнена с операндом, лежащим вне
допустимых границ адреса;
3 Точное количество зависит от модели процессора.
□ 6 — "Invalid opcode" (Недопустимый код операции), ошибка. Выполняю-
Выполняющий блок процессора обнаружил недопустимый код операции (часть ма-
машинной инструкции, определяющая выполняемую операцию);
□ 7 — "Device not available" (Устройство недоступно), ошибка. Была
выполнена инструкция ESCAPE, MMX и SSE/SSE2 при установленном
флаге ts в регистре его (см. главу 3);
П 8 — "Double fault" (Двойная ошибка), авария. Обычно, когда процессор
получает исключение при попытке вызвать обработчик для предыдущего
исключения, эти два исключения могут быть обработаны последователь-
последовательно. Однако в некоторых случаях процессор не может обработать их после-
последовательно и возбуждает это исключение;
□ 9 — "Coprocessor segment overrun" (Нарушение в сегменте сопроцессора),
авария. Проблемы с внешним математическим сопроцессором (относится
только к старым микропроцессорам 80386);
□ 10— "Invalid TSS" (Недопустимый сегмент состояния задачи), ошибка.
Процессор пытался выполнить переключение контекста для процесса,
имеющего недопустимый сегмент состояния задачи;
□ 11 — "Segment not present" (Сегмент отсутствует), ошибка. Попытка со-
сослаться на сегмент, отсутствующий в памяти (такой, у которого сброшен
флаг segment-Present в дескрипторе сегмента);
□ 12— "Stack segment fault" (Ошибка в сегменте стека), ошибка. Инструк-
Инструкция попыталась выйти за пределы сегмента стека, или сегмент, идентифи-
идентифицируемый полем ss, отсутствует в памяти;
□ 13 — "General protection" (Общий сбой защиты), ошибка. Нарушено одно
из правил доступа в защищенном режиме 80x86;
□ 14— "Page Fault" (Ошибка обращения к странице), ошибка. Обращение к
странице, отсутствующей в памяти, или соответствующая запись Таблицы
Страниц содержит нули, или произошло нарушение механизма защиты
выделения страниц;
□ 15 — Зарезервирован компанией Intel;
□ 16— "Floating-point error" (Ошибка операции с плавающей точкой),
ошибка. Блок выполнения операций с плавающей точкой, встроенный в
чип процессора, просигнализировал об ошибке, такой как переполнение
или деление на О4;
4 Микропроцессоры 80x86 генерируют это исключение также при выполнении деления со знаком,
когда результат не может быть сохранен в виде целого со знаком (например, при делении
-2 147 483 648 на-1).
□ 17— "Alignment check" (Сбой проверки выравнивания), ошибка. Адрес
операнда не выровнен должным образом (например, адрес длинного цело-
целого не кратен 4);
□ 18 — "Machine check" (Сбой машинной проверки), авария. Механизм ма-
машинной проверки обнаружил ошибку процессора или шины;
□ 19 — "SIMD floating point exception" (Исключение при выполнении SIMD-
операции с плавающей точкой), ошибка. Блок SSE или SSE2, встроенный
в процессор, просигнализировал об ошибке при выполнении операции с
плавающей точкой.
Значения с 20 до 31 зарезервированы компанией Intel для будущих разрабо-
разработок. Как показано в табл. 4.1, каждое исключение обслуживается соответст-
соответствующим обработчиком (см. разд. "Обработка исключений" далее в этой гла-
главе), который обычно посылает сигнал Unix процессу, вызвавшему исклю-
исключение.
Таблица 4.1. Сигналы, посылаемые обработчиками исключений
№ Исключение Обработчик исключения Сигнал
0 Divide error (Ошибка деления divideerror () sigfpe
на ноль)
1 Debug (Отладка) debug () SIGTRAP
2 NMI nmi() Нет
3 Breakpoint (Точка останова) int3() sigtrap
4 Overflow (Переполнение) overflow () sigsegv
5 Bounds check (Сбой проверки границ) bounds () sigsegv
6 Invalid opcode (Недопустимый код invalid_op () sigill
операции)
7 Device not available (Устройство device_not_available () Нет
недоступно)
8 Double fault (Двойная ошибка) double faultfn () Нет
9 Coprocessor segment overrun coprocessor_segment_overrun () SIGFPE
(Нарушение в сегменте
сопроцессора)
10 Invalid TSS (Недопустимый сегмент invalidTSS () sigsegv
состояния задачи)
11 Segment not present segment_not_present () SIGBUS
(Сегмент отсутствует)
12 Stack segment fault (Ошибка stack_segment () SIGBUS
в сегменте стека)
Таблица 4.1 (окончание)
№ Исключение Обработчик исключения Сигнал
13 General protection (Общий сбой general_protection() SIGSEGV
защиты)
14 Page Fault (Ошибка обращения page_fault () SIGSEGV
к странице)
15 Зарезервирован компанией Intel Нет Нет
16 Floating-point error (Ошибка операции coprocessor_error () SIGFPE
с плавающей точкой)
17 Alignment check (Сбой проверки alignment_check() SIGBUS
выравнивания)
18 Machine check (Сбой машинной machinecheck () Нет
проверки)
19 SIMD floating point (Исключение simd_coprocessor_error () SIGFPE
при выполнении SIMD-операции
с плавающей точкой)
Таблица дескрипторов прерываний
Системная таблица, называемая таблицей дескрипторов прерываний, или
IDT (Interrupt Descriptor Table), ассоциирует каждый вектор прерывания или
исключения с адресом соответствующего обработчика. Таблица дескрипто-
дескрипторов прерываний должна быть нужным образом проинициализирована до то-
того, как ядро включит прерывания.
Формат таблицы IDT аналогичен формату таблиц GDT и LDT, описанных в
главе 2. Каждая запись соответствует вектору прерывания или исключения и
включает в себя 8-байтовый дескриптор. Следовательно, для хранения табли-
таблицы дескрипторов прерываний требуется максимум 256 х8 = 2048 байтов.
Процессорный регистр idtr позволяет разместить таблицу дескрипторов пре-
прерываний в любом месте памяти. Он содержит как базовый физический адрес
таблицы, так и ее максимальную длину. Регистр должен быть инициализиро-
инициализирован с помощью ассемблерной инструкции lidt до включения прерываний.
Таблица дескрипторов прерываний может содержать дескрипторы трех ти-
типов. На рис. 4.2 проиллюстрировано содержимое 64 битов каждого дескрип-
дескриптора. В частности, значение поле туре, закодированное в битах 40—43, иден-
идентифицирует тип дескриптора.
В таблице содержатся следующие дескрипторы:
□ дескриптор шлюза задач — включает в себя селектор TSS-сегмента про-
процесса, который должен заместить текущий, когда возникнет сигнал пре-
прерывания;
Рис. 4.2. Формат дескрипторов шлюзов
□ дескриптор шлюза прерываний — включает в себя селектор сегмента и
смещение внутри сегмента обработчика прерывания или исключения. При
передаче управления соответствующему сегменту процессор сбрасывает
флаг if, тем самым отключая последующие маскируемые прерывания;
□ дескриптор шлюза ловушек — аналогичен предыдущему, но при передаче
управления нужному сегменту процессор не изменяет флаг if.
Как мы увидим в разд. "Шлюзы прерываний, ловушек и системы" далее в
этой главе, Linux использует шлюзы прерываний для обработки прерываний,
а шлюзы ловушек — для обработки исключений5.
Аппаратная обработка прерываний и исключений
Сейчас мы покажем, как управляющий блок процессора обрабатывает пре-
прерывания и исключения. Мы будем предполагать, что ядро уже инициализи-
инициализировано, и поэтому процессор работает в защищенном режиме.
5 Исключение "Двойная ошибка", свидетельствующее о неправильном поведении ядра, является
единственным исключением, которое обрабатывается шлюзом задач.
После выполнения очередной инструкции пара регистров cs и eip содержит
логический адрес инструкции, которая должна быть выполнена следующей.
Перед тем как перейти к ней, управляющий блок проверяет, не возникло ли
прерывание или исключение, пока он выполнял предыдущую. Если возникло,
управляющий блок процессора выполняет следующие действия:
1. Определяет вектор / @</<255), ассоциированный с этим прерыванием
или исключением.
2. Читает /-ю запись таблицы дескрипторов прерываний, на которую указы-
указывает регистр idtr (далее мы предполагаем, что запись содержит дескрип-
дескриптор шлюза прерывания или ловушки).
3. Получает базовый адрес таблицы GDT из регистра gdtr и читает в ней де-
дескриптор сегмента, идентифицируемый селектором в записи таблицы де-
дескрипторов прерываний. Этот дескриптор содержит базовый адрес сег-
сегмента, содержащего обработчик прерывания или исключения.
4. Убеждается, что прерывание было возбуждено источником, имеющим на
это право. Во-первых, управляющий блок сравнивает текущий уровень
привилегий (CPL, Current Privilege Level), который хранится в двух млад-
младших битах регистра cs, с уровнем привилегий дескриптора (DPL,
Descriptor Privilege Level), дескриптора сегмента, прочитанного из табли-
таблицы GDT. Затем управляющий блок возбуждает исключение "Общий сбой
защиты", если текущий уровень привилегий меньше уровня привилегий
дескриптора, потому что обработчик прерываний не может быть менее
привилегированным, чем программа, вызвавшая прерывание. Для про-
программных исключений управляющий блок производит дополнительную
проверку: сравнивает текущий уровень привилегий с уровнем привилегий
дескриптора шлюза из таблицы дескрипторов прерываний и возбуждает
исключение "Общий сбой защиты", если уровень привилегий дескриптора
меньше текущего уровня привилегий. Эта последняя проверка позволяет
предотвратить доступ со стороны пользовательских приложений к некото-
некоторым конкретным шлюзам ловушек или прерываний.
5. Проверяет, имеет ли место изменение уровня привилегий, т. е., отличается
ли текущий уровень привилегий от уровня привилегий выбранного деск-
дескриптора сегмента. Если это так, управляющий блок должен воспользо-
воспользоваться стеком, ассоциированным с новым уровнем привилегий. Чтобы
сделать это, управляющий блок выполняет следующие действия:
• читает содержимое регистра tr, чтобы получить доступ к TSS-сегменту
текущего процесса;
• загружает в регистры ss и esp соответствующие значения сегмента сте-
стека и указателя стека, ассоциированные с новым уровнем привилегий.
Эти значения хранятся в сегменте TSS (см. главу 3);
• в новом стеке сохраняет предыдущие значения регистров ss и esp? ко-
которые определяют логический адрес стека, ассоциированного со ста-
старым уровнем привилегий.
6. Если возникла ошибка, управляющий блок загружает в регистры cs и eip
логический адрес инструкции, вызвавшей исключение, чтобы была воз-
возможность выполнить ее снова.
7. Сохраняет в стеке содержимое регистров ef lags, cs и eip.
8. Если исключение несет в себе код аппаратной ошибки, управляющий блок
сохраняет его в стеке.
9. Загружает в регистры cs и eip, соответственно, значения селектора сег-
сегмента и смещения из дескриптора шлюза, хранящихся в /-й записи табли-
таблицы дескрипторов прерываний. Эти значения определяют логический адрес
первой инструкции обработчика прерывания или исключения.
Последний шаг, выполненный управляющим блоком, эквивалентен передаче
управления обработчику прерывания или исключения. Иными словами, ин-
инструкция, выполняемая управляющим блоком после аппаратной обработки
сигнала прерывания, является первой инструкцией выбранного обработчика.
После обработки прерывания или исключения, соответствующий обработчик
должен возвратить управление прерванному процессу. Этой цели служит ин-
инструкция iret, которая заставляет управляющее устройство выполнить сле-
следующие действия:
1. Загрузить в регистры cs, eip и efiags значения, сохраненные в стеке. Если
код аппаратной ошибки был помещен в стек выше содержимого регистра
eip, он должен быть извлечен из стека до выполнения инструкции iret.
2. Проверить, равен ли текущий уровень привилегий обработчика значению,
хранящемуся в двух младших разрядах регистра cs (это будет означать,
что прерванный процесс выполнялся на том же уровне привилегий, что и
обработчик). Если это так, инструкция iret завершает выполнение; в про-
противном случае происходит переход на следующий шаг.
3. Загрузить в регистры ss и esp значения, сохраненные в стеке, и вернуться
к стеку, ассоциированному с прежним уровнем привилегий.
4. Изучить содержимое сегментных регистров ds, es, fs и gs. Если какой-
либо из них содержит селектор, ссылающийся на дескриптор сегмента,
уровень привилегий которого ниже текущего уровня привилегий, необхо-
необходимо очистить этот регистр. Управляющий блок должен сделать это, что-
чтобы запретить программам пользовательского режима, которые работают с
текущим уровнем привилегий 3, использовать сегментные регистры, кото-
которыми до этого пользовались процедуры ядра (с уровнем привилегий деск-
риптора, равным 0). Если эти регистры не очистить, злонамеренные про-
программы режима пользователя могут эксплуатировать их с целью получе-
получения доступа к адресному пространству ядра.
Вложенное выполнение
обработчиков исключений и прерываний
Каждое прерывание или исключение приводит к выполнению управляющего
тракта ядра, т. е. самостоятельной последовательности инструкций, выпол-
выполняемых "от имени" текущего процесса. Например, когда устройство вво-
ввода/вывода возбуждает прерывание, первые инструкции управляющего тракта
ядра сохраняют содержимое регистров процессора в стеке режима ядра,
а последние — извлекают эти значения и восстанавливают содержимое реги-
регистров.
Управляющие тракты ядра могут быть произвольным образом вложены друг
в друга. Обработчик прерывания может быть прерван другим обработчиком,
что приведет к вложенному выполнению управляющих трактов ядра, как по-
показано на рис. 4.3. В результате последние инструкции управляющего тракта
ядра, обслуживающего прерывание, не всегда возвращают текущий процесс в
режим пользователя. Если уровень вложенности больше 1, эти инструкции
приведут к возобновлению управляющего тракта ядра, который был прерван
последним, и процессор продолжит работу в режиме ядра.
Рис. 4.3. Пример вложенного выполнения управляющих потоков ядра
Цена, которую приходится платить за возможность вложенного выполнения
управляющих трактов ядра, состоит в том, что обработчик прерывания нельзя
блокировать. Иными словами, пока обработчик прерывания выполняется,
нельзя производить переключение процессов. Дело в том, что все данные,
необходимые для возобновления вложенного управляющего тракта ядра,
хранятся в стеке режима ядра, который жестко привязан к текущему про-
процессу.
Если предположить, что в коде ядра нет программистских ошибок, большин-
большинство исключений может возникнуть, только когда процессор работает в ре-
режиме пользователя. Действительно, они вызываются либо ошибками в про-
программах, либо командами отладчика. Однако исключение "Ошибка обраще-
обращения к странице" может возникнуть в режиме ядра. Это происходит, когда
процесс пытается обратиться к странице, принадлежащей его адресному про-
пространству, но в данный момент отсутствующей в оперативной памяти. При
обработке такого исключения ядро может приостановить текущий процесс и
заменить его другим, пока запрошенная страница не станет доступной.
Управляющий тракт ядра, который обрабатывает исключение "Ошибка об-
обращения к странице" возобновит свое выполнение, как только процессу снова
будет предоставлено процессорное время.
Поскольку обработчик исключения "Ошибка обращения к странице" никогда
не возбуждает дальнейшие исключения, вложенными могут быть максимум
два управляющих тракта ядра (первый был инициирован системным вызо-
вызовом, а второй — исключением).
В противоположность исключениям прерывания, генерируемые устройства-
устройствами ввода/вывода, не пользуются структурами, содержащими данные, специ-
специфичные для текущего процесса, хотя управляющие тракты ядра, которые их
обрабатывают, выполняются от имени этого процесса. На практике невоз-
невозможно предсказать, какой процесс будет работать, когда возникнет то или
иное прерывание.
Обработчик прерывания может вытеснить как другие обработчики прерыва-
прерываний, так и обработчики исключений. Обработчик исключения, наоборот, ни-
никогда не вытесняет обработчик прерываний. Единственным исключением,
которое может быть возбуждено в режиме ядра, является "Ошибка обраще-
обращения к странице", которую мы только что упоминали. Однако обработчики
прерываний никогда не выполняют операции, которые вызывают ошибки
обращения к страницам и, как результат, потенциальное переключение про-
процессов.
Linux чередует управляющие тракты ядра по двум важным причинам:
□ чтобы повысить производительность программируемых контроллеров
прерываний и контроллеров устройств. Представим, что контроллер уст-
устройства посылает сигнал по IRQ-линии. Программируемый контроллер
прерываний преобразует его во внешнее прерывание, а затем оба контрол-
контроллера простаивают в ожидании, пока программируемый контроллер преры-
прерываний получит подтверждение от процессора. Благодаря чередованию
управляющих трактов, ядро может отправить подтверждение, даже во
время обработки предыдущего прерывания;
□ чтобы реализовать модель прерываний без уровней приоритетов. По-
Поскольку выполнение каждого обработчика прерываний может быть отло-
жено другим, нет необходимости устанавливать фиксированные приори-
приоритеты для аппаратных устройств. Это упрощает код ядра и повышает его
переносимость.
В многопроцессорных системах несколько управляющих трактов ядра могут
работать параллельно. Более того, управляющий тракт, ассоциированный с
исключением, может начаться на одном процессоре, а затем в результате пе-
переключения процессов мигрировать на другой процессор.
Инициализация
таблицы дескрипторов прерываний
Теперь, когда мы знаем, как микропроцессоры 80x86 работают с прерыва-
прерываниями и исключениями на аппаратном уровне, мы можем перейти к описа-
описанию инициализации таблицы дескрипторов прерываний.
Вспомним, что перед включением прерываний ядро должно загрузить на-
начальный адрес таблицы дескрипторов прерываний в регистр idtr и инициа-
инициализировать все записи этой таблицы. Это происходит на этапе инициализа-
инициализации системы (см. приложение 1).
Инструкция int позволяет процессу, выполняющемуся в режиме пользовате-
пользователя, генерировать сигнал на прерывание с произвольным вектором в диапазо-
диапазоне от 0 до 255. Поэтому инициализация таблицы IDT должна быть выполнена
аккуратно, чтобы не пропустить незаконные прерывания и исключения, си-
симулированные процессами в режиме пользователя с помощью инструкций
int. Этого можно достичь, записав ноль в поле DPL соответствующего деск-
дескриптора шлюза прерывания или ловушки. Если процесс пытается сгенериро-
сгенерировать один из таких сигналов прерывания, управляющий блок сравнит теку-
текущий уровень привилегий с уровнем привилегий дескриптора и возбудит ис-
исключение "Общий сбой защиты".
Однако в отдельных случаях процесс в пользовательском режиме должен
иметь возможность сгенерировать программное исключение. Чтобы позво-
позволить ему это, достаточно записать в поле DPL соответствующего дескриптора
шлюза прерывания или ловушки число 3, т. е. максимально возможное зна-
значение.
Рассмотрим, как Linux реализует эту стратегию.
Шлюзы прерываний, ловушек и системы
Как было сказано ранее, в разд. "Таблица дескрипторов прерываний", архи-
архитектура Intel обеспечивает три типа дескрипторов прерываний: дескрипторы
шлюзов задач, прерываний и ловушек. В Linux используется немного другая
классификация и терминология, когда речь заходит о дескрипторах прерыва-
прерываний, хранящихся в таблице IDT:
□ шлюз прерывания — шлюз прерывания в терминах Intel, недоступный
процессу режима пользователя (поле DPL у шлюза равно 0). В Linux все
обработчики прерываний активизируются с помощью шлюзов прерыва-
прерываний, и все они ограничены режимом ядра;
Я шлюз системы — шлюз ловушки в терминах Intel, доступный процессу
пользовательского режима (поле DPL у шлюза равно 3). Три обработчика
исключений в Linux с векторами 4, 5 и 128 активизируются при помощи
шлюзов системы, и, следовательно, три ассемблерных инструкции into,
bound и int $0x80 могут быть выполнены в режиме пользователя;
□ шлюз системного прерывания — шлюз прерывания в терминах Intel, дос-
доступный процессу пользовательского режима (поле DPL у шлюза равно 3).
Обработчик исключения, ассоциированный с вектором 3, активизируется
при помощи шлюза системного прерывания, следовательно, ассемблерная
инструкция int3 может быть выполнена в режиме пользователя;
□ шлюз ловушки — шлюз ловушки в терминах Intel, недоступный процессу
пользовательского режима (поле DPL у шлюза равно 0). Большинство об-
обработчиков исключений в Linux активизируется при помощи шлюзов ло-
ловушек;
□ шлюз задачи— шлюз задачи в терминах Intel, недоступный процессу
пользовательского режима (поле DPL у шлюза равно 0). В Linux обработ-
обработчик исключения "Двойная ошибка" активизируется при помощи шлюза
задачи.
Для занесения шлюзов в таблицу дескрипторов прерываний служат следую-
следующие архитектурно-зависимые функции:
□ set_intr_gate(n,addr) — заносит шлюз прерывания в n-ю запись таблицы
дескрипторов прерываний. Селектор сегмента у шлюза равен селектору
сегмента кода ядра. Поле "смещение" равно addr, т. е. адресу обработчика
прерывания. Поле DPL содержит 0;
□ set_system_gate(n,addr) — заносит ШЛЮЗ ЛОВушКИ В n-Ю запись таблицы
дескрипторов прерываний. Селектор сегмента у шлюза равен селектору
сегмента кода ядра. Поле "смещение" равно addr, т. е. адресу обработчика
исключения. Поле DPL содержит 3;
□ set_system_intr_gate(n,addr) — занОСИТ ШЛЮЗ прерывания В n-Ю запись
таблицы дескрипторов прерываний. Селектор сегмента у шлюза равен се-
селектору сегмента кода ядра. Поле "смещение" равно addr, т. е. адресу об-
обработчика исключения. Поле DPL содержит 3;
□ set_trap_gate(n,addr) — аналогична предыдущей функции, но поле DPL
содержит 0;
□ set_task_gate(n,gdt) — заносит шлюз задачи в n-ю запись таблицы деск-
рипторов прерываний. Селектор сегмента у шлюза содержит индекс в таб-
таблице GDT для селектора сегмента TSS, содержащего функцию, которая
должна быть активизирована. Поле "смещение" содержит 0, а поле DPL
содержит 3.
Предварительная инициализация таблицы ЮТ
Таблица дескрипторов прерываний инициализируется и используется про-
процедурами BIOS, пока компьютер работает в реальном режиме. Однако когда
Linux приступает к работе, таблица дескрипторов прерываний переносится в
другую область оперативной памяти и инициализируется второй раз, потому
что Linux не пользуется ни одной из процедур BIOS (см. приложение 1).
Таблица дескрипторов прерываний хранится в виде таблицы idttabie, со-
состоящей из 256 записей. Шестибайтовая переменная idtdescr содержит раз-
размер и адрес таблицы дескрипторов прерываний и используется на этапе ини-
инициализации системы, когда ядро загружает данные в регистр idtr с помощью
ассемблерной инструкции lidt6.
Во время инициализации ядра ассемблерная функция setupidt () начинает
работу с заполнения всех 256 записей таблицы idttabie одним и тем же
шлюзом прерываний, который ссылается на обработчик прерываний
ignore__int ()'.
setup_idt:
lea ignore_int, %edx
movl $( KERNEL_CS « 16), %eax
mow %dx, %ax /* селектор = 0x0010 = cs */
movw $0x8e00, %dx /* шлюз прерывания, dpl=0, присутствует */
lea idt_table, %edi
mov $256, %ecx
rp_sidt:
movl %eax, (%edi)
movl %edx, 4(%edi)
addl $8, %edi
6 В некоторых старых моделях Pentium имеется печально известная ошибка "fOOf', которая позво-
позволяет программам режима пользователя вызывать зависание системы. Работая на таких процессорах,
Linux использует обходной путь, основанный на инициализации регистра idtr фиксированно
отображенным линейным адресом, доступным только для чтения, который указывает на реальную
таблицу IDT.
dec %ecx
jne rp_sidt
ret
Обработчик прерывания ignore_int(), написанный на ассемблере, можно
рассматривать как заглушку, которая выполняет следующие действия:
1. Сохраняет в стеке содержимое некоторых регистров.
2. Вызывает функцию printko, которая выводит системное сообщение
"Unknown interrupt" (неизвестное прерывание).
3. Восстанавливает содержимое регистров из стека.
4. Выполняет инструкцию iret, чтобы возобновить прерванную программу.
Обработчик ignoreinto никогда не должен выполняться. Появление сооб-
сообщений "Unknown interrupt" на консоли или в журнальных файлах свидетель-
свидетельствует либо об аппаратной проблеме (устройство ввода/вывода выдает не-
непредвиденные прерывания), либо о проблеме в ядре (прерывание или исклю-
исключение не было обработано должным образом).
Вслед за этой предварительной инициализацией ядро делает второй проход
по таблице дескрипторов прерываний, чтобы заменить некоторые пустые об-
обработчики на осмысленные обработчики ловушек и прерываний. Когда оно
закончит, таблица будет содержать специализированный шлюз ловушки,
прерывания или системы отдельно для каждого исключения, возбуждаемого
управляющим блоком, и для каждого запроса IRQ, распознаваемого контрол-
контроллером прерываний.
В следующих двух разделах подробно описано, как это делается для исклю-
исключений и прерываний.
Обработка исключений
Большинство исключений, возбуждаемых процессором, интерпретируется
системой Linux как ошибочные состояния. Когда возникает одно из них, ядро
посылает сигнал процессу, вызвавшему исключение, чтобы уведомить его об
аномальной ситуации. Если, например, процесс выполняет деление на ноль,
процессор возбуждает исключение "Ошибка деления на ноль", и соответст-
соответствующий обработчик исключения посылает сигнал sigfpe текущему процессу,
который предпринимает необходимые шаги, чтобы выйти из ситуации или
(если для этого сигнала не установлен обработчик) закончить выполнение
аварийно.
Однако имеется пара случаев, в которых Linux эксплуатирует исключения,
возбуждаемые процессором, для более эффективного управления аппарат-
ными ресурсами. Первый случай был описан в главе 3. Исключение "Устрой-
"Устройство недоступно" используется в комбинации с флагом ts регистра его, чтобы
заставить ядро загрузить новые значения в регистры, используемые процес-
процессором для операций с плавающей точкой. Второй случай относится к исклю-
исключению "Ошибка обращения к странице", которое используется, чтобы ядро
могло отложить выделение процессу новых страничных кадров до самого
последнего момента. Соответствующий обработчик довольно сложен, потому
что это исключение может означать ошибочную ситуацию (см. разд. "Об-
"Обработчик исключения "ошибка обращения к странице "" главы 9).
Обработчики исключений имеют стандартную структуру, и их выполнение
состоит из трех шагов:
1. Сохранение содержимого большинства регистров в стеке режима ядра (эта
часть кодируется на ассемблере).
2. Обработка исключения с помощью функции на языке С.
3. ВЫХОД ИЗ Обработчика С ПОМОЩЬЮ фуНКЦИИ ret_from_exception ().
Чтобы воспользоваться исключениями, таблица дескрипторов исключений
должна быть проинициализирована так, чтобы для каждого распознаваемого
исключения была указана функция-обработчик. Задача функции trapinit ()
заключается в занесении окончательной информации (функций, обрабаты-
обрабатывающих исключения) во все записи таблицы дескрипторов прерываний, от-
относящиеся к немаскируемым прерываниям и исключениям. Это делается
С ПОМОЩЬЮ функций set_trap_gate (), set_intr_gate (), set_system_gate (),
set_system_intr_gate () И set_task_gate ():
set_trap_gate @, ÷_error) ;
set_trap_gateA,&debug);
set_intr_gateB, &nmi) ;
set_system_intr_gateC,&int3);
set_system_gateD,&overflow);
set_system_gateE,&bounds);
set_trap_gateF,&invalid_op);
set_trap_gateG,&device_not_available);
set_task_gate(8, 31) ;
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gateA0,&invalid_TSS);
set_trap_gate A1, &segment_not_present) ;
set_trap_gate A2, &stack_segment) ;
set_trap_gateA3,&general_protection);
set_intr_gateA4,&page_fault);
set_trap_gateA6,&coprocessor_error);
set_trap_gateA7,&alignment_check);
set_trap_gate A8, &machine_check) ;
set_trap_gate A9, &simd_coprocessor_error) ;
set_system_gateA28,&system_call) ;
Исключение "Двойная ошибка" обрабатывается с применением шлюза зада-
задачи, а не ловушки или системы, поскольку оно свидетельствует о серьезных
проблемах в ядре. Поэтому обработчик исключения, который пытается вы-
вывести содержимое регистров, не доверяет текущему значению в регистре esp.
Когда возникает это исключение, процессор читает дескриптор шлюза зада-
задачи, хранящийся в таблице дескрипторов прерываний в записи с индексом 8.
Этот дескриптор указывает на дескриптор специального сегмента TSS, хра-
хранящийся в 32-й записи таблицы GDT. Затем процессор загружает в регистре
eip и esp значения, хранящиеся в соответствующих сегментах TSS. В резуль-
результате Процессор ВЫПОЛНЯеТ Обработчик ИСКЛЮЧеНИЯ doublefault_fn() СО СВОИМ
собственным стеком.
Теперь рассмотрим, что делает типичный обработчик исключения. Это опи-
описание обработки исключения будет достаточно кратким. В частности, оно не
сможет охватить:
□ коды сигналов (см. табл. 11.8), посылаемых некоторыми обработчиками
процессам режима пользователя;
□ исключения, возникающие, когда ядро работает в режиме эмуляции MS-
DOS (режим vm86), поскольку они требуют отдельной обработки;
□ "отладочные" исключения.
Сохранение регистров
для обработчика исключений
Воспользуемся именем handiername ("имя обработчика") для обозначения
типичного обработчика исключений. (Фактические имена всех обработчиков
приведены в списке макросов в предыдущем разделе.)
Каждый обработчик исключения начинается со следующих ассемблерных
инструкций:
handle r_name:
pushl $0 /* только для некоторых исключений */
pushl $do handler name
jmp error_code
Если не предполагается, что управляющий блок автоматически занесет код
аппаратной ошибки в стек, когда возникнет исключение, то соответствующий
ассемблерный фрагмент включает в себя инструкцию pushl $o, записываю-
записывающую ноль в стек. Затем в стек помещается адрес функции на языке С, имя
которой образовано именем обработчика исключения и префиксом do_.
У всех обработчиков исключений, кроме обработчика "Устройство недоступ-
недоступно" (см. главу 3), ассемблерной код, обозначенный меткой errorcode, одина-
одинаков. Этот код выполняет следующие действия:
1. Сохраняет регистры, которые может изменить функция на языке С, нахо-
находящаяся на вершине стека.
2. Выдает инструкцию cid, чтобы сбросить флаг направления df в регистре
efiags. Это гарантирует автоматическое увеличение регистров edi и esi
при работе со строковыми инструкциями7.
3. Копирует код аппаратной ошибки, сохраненный в стеке по адресу esp+Зб,
в регистр edx. Записывает-1 по этому же адресу. Как мы увидим в
разд. "Повторное выполнение системных вызовов" главы 11, это значение
служит для отличия исключений 0x8о от других исключений.
4. Записывает в регистр edi адрес функции dohandier _name(), сохраненный
в стеке по адресу esp+32, а в это место стека заносит содержимое регист-
регистра es.
5. Записывает в регистр еах текущий адрес вершины стека режима ядра. Этот
адрес идентифицирует ячейку памяти, содержащую последнее значение
регистра, сохраненное на шаге 1.
6. Загружает селектор сегмента пользовательских данных в регистры ds и es.
7. Вызывает функцию на языке С, адрес которой теперь хранится в регист-
регистре edi.
Вызванная функция считывает свои аргументы из регистров еах и edx, а не из
стека. Мы уже встречали функцию, которая получает аргументы из регистров
процессора. Это была функция switchto (), описанная в главе 3.
Вход и выход из обработчика исключений
Как мы уже говорили, имена функций на языке С, реализующих обработчики
исключений, всегда состоят из префикса do_, за которым следует имя обра-
обработчика. Большинство этих функций вызывает функцию dotrapO, чтобы
сохранить код аппаратной ошибки и вектор исключения в дескрипторе теку-
текущего процесса, а затем отправить этому процессу соответствующий сигнал:
current->thread.error_code = error_code;
current->thread.trap_no = vector;
force_sig(sig_number, current);
7 Одна ассемблерная "строковая инструкция", например rep;movsb, способна воздействовать на
целый блок данных (строку).
Текущий процесс приступает к обработке сигнала сразу после завершения
обработчика исключения. Сигнал будет обслужен либо в режиме пользовате-
пользователя обработчиком, принадлежащим процессу (если таковой существует), либо
в режиме ядра. Во втором случае ядро обычно уничтожает процесс (см. гла-
главу 11). Сигналы, посылаемые обработчиками исключений, перечислены в
табл. 4.1.
Обработчик исключений всегда проверяет, возникло ли исключение в режи-
режиме пользователя или в режиме ядра, а в последнем случае — послужил ли
причиной исключения недопустимый аргумент системного вызова.
В разд. "Динамическая проверка адресов: код обработки исключения11 гла-
главы 10 мы поясним, как ядро защищается от недопустимых аргументов, пере-
передаваемых системным вызовам. Любое другое исключение, возникшее в ре-
режиме ядра, можно объяснить только ошибкой в коде ядра. В этом случае об-
обработчик исключения понимает, что ядро ведет себя неправильно. Чтобы
избежать порчи данных на жестких дисках, обработчик вызывает функцию
die (), которая выводит на консоль содержимое всех регистров процессора
(такой дамп называется "kernel oops"), и завершает текущий процесс, вызвав
функцию doexit () (см. главу 3).
Когда функция на языке С, реализующая обработку исключения, завершает
работу, выполняется инструкция jmp, т. е. переход на функцию retf rom_
exception(). Эта функция описана в разд. "Возврат из прерываний и исклю-
исключений" далее в этой главе.
Обработка прерываний
Как мы говорили ранее, обработка большинства исключений сводится к от-
отправке сигнала Unix процессу, вызвавшему исключение. Таким образом, дей-
действия, которые необходимо предпринять, откладываются до того момента,
когда процесс примет сигнал. В результате ядро способно быстро "расправ-
"расправляться" с исключениями.
Этот подход неприемлем для прерываний, потому что они часто поступают
намного позже момента приостановки процесса, к которому относятся (на-
(например, процесса, запросившего пересылку данных), когда уже работает со-
совершенно другой процесс. Поэтому нет никакого смысла посылать Unix-
сигнал текущему процессу.
Обработка прерывания зависит от его типа. В дальнейшем мы будем разли-
различать три основных класса прерываний:
□ прерывания ввода/вывода — устройство ввода/вывода требует к себе вни-
внимания. Соответствующий обработчик прерывания должен опросить уст-
ройство, чтобы определить дальнейшие действия. Этот тип прерываний
мы опишем в разд. "Обработка прерываний ввода/вывода" далее в этой
главе;
П прерывания по таймеру— некоторый таймер (либо локальный таймер
усовершенствованного программируемого контроллера прерываний, либо
внешний таймер) выдал прерывание. Этот тип прерываний уведомляет яд-
ядро об истечении установленного интервала времени. Такие прерывания
обрабатываются, в основном, как прерывания ввода/вывода. Конкретные
характеристики прерываний по таймеру обсуждаются в главе 6;
П межпроцессорные прерывания — процессор отправил прерывание друго-
другому процессору многопроцессорной системы. Подобные прерывания обсу-
обсуждаются в разд. "Обработка межпроцессорных прерываний" далее в этой
главе.
Обработка прерываний ввода/вывода
Вообще говоря, обработчик прерываний ввода/вывода должен быть доста-
достаточно гибким, чтобы обслужить несколько устройств одновременно. Напри-
Например, в архитектуре, использующей шину PCI, несколько устройств могут со-
совместно использовать одну IRQ-линию. Это означает, что вектор прерывания
сам по себе не отражает всю картину. В примере, приведенном в табл. 4.3,
вектор 43 присвоен одновременно и порту USB, и звуковой карте. Однако в
более старых архитектурах персональных компьютеров (например, ISA) не-
некоторые аппаратные устройства не будут работать надежно, если их IRQ-
линию используют другие устройства.
Гибкость обработчика прерываний достигается двумя разными способами:
□ совместное использование IRQ-линии— обработчик прерываний выпол-
выполняет несколько служебных процедур обработки прерываний. Каждая такая
процедура является функцией, ассоциированной с одним из устройств, со-
совместно использующих IRQ-линию. Поскольку заранее не известно, какое
именно устройство выдало запрос на прерывание, выполняется каждая
служебная процедура, которая проверяет, требует ли ее устройство вни-
внимания. Если требует, она выполняет все действия, необходимые, когда
данное устройство возбуждает прерывание;
□ динамическое выделение IRQ-линии — IRQ-линия ассоциируется с драй-
драйвером устройства в самый последний момент. Например, IRQ-линия нако-
накопителя на гибких дисках выделяется, только когда пользователь обратится
к дисководу. Таким образом, один и тот же вектор IRQ может быть ис-
использован несколькими аппаратными устройствами, даже если они не мо-
могут совместно владеть IRQ-линией. Конечно, при этом к ним нельзя будет
обращаться одновременно.
Не все действия, предпринимаемые в ответ на прерывание, имеют одинако-
одинаковую степень срочности. На самом деле, сам обработчик прерывания не явля-
является подходящим местом для всех видов действий. Выполнение длительных
некритических операций должно быть отложено, потому что до завершения
обработчика прерываний все сигналы на соответствующей IRQ-линии вре-
временно игнорируются. Что еще важнее, процесс, от имени которого выполня-
выполняется обработчик прерывания, должен оставаться в состоянии taskrunning,
чтобы не произошло зависание системы. Следовательно, обработчики преры-
прерываний не могут выполнять никакие блокирующие процедуры, например, дис-
дисковые операции ввода/вывода. Действия, выполняемые в ответ на прерыва-
прерывания, подразделяются в Linux на три класса:
□ критические— действия, примерами которых являются подтверждение
приема прерывания на программируемом контроллере прерываний, пере-
перепрограммирование этого контроллера или контроллера устройства, а так-
также обновление структур, к которым обращаются как устройство, так и
процессор. Эти действия можно выполнить быстро, и они являются кри-
критическими, поскольку должны быть совершены как можно скорее. Крити-
Критические действия выполняются в коде обработчика прерываний, а маски-
маскируемые прерывания при этом отключены;
□ некритические — действия, такие как обновление структур данных, к ко-
которым обращается только процессор (например, считывание скан-кода по-
после нажатия на клавишу). Эти действия тоже можно выполнить быстро,
поэтому обработчик прерываний совершает их немедленно, при включен-
включенных прерываниях;
□ некритические, допускающие отложенное выполнение — действия, такие
как копирование содержимого буфера в адресное пространство процесса
(например, отправка буфера клавиатуры процессу, отвечающему за под-
поддержку терминала). Эти действия можно отложить на длительный срок,
никак не повлияв на операции ядра. Заинтересованный процесс просто бу-
будет продолжать ждать данные. Некритические, допускающие отложенное
выполнение действия обслуживаются отдельными функциями, которые
обсуждаются в разд. "Softirq-функции и тасклеты " далее в этой главе.
Независимо от того, какое устройство вызвало прерывание, все обработчики
прерываний ввода/вывода выполняют одинаковые базовые действия:
1. Сохраняют значение IRQ и содержимое регистров в стеке режима ядра.
2. Отправляют сигнал подтверждения программируемому контроллеру пре-
прерываний, обслуживающему IRQ-линию. Тем самым он получает разреше-
разрешение генерировать дальнейшие прерывания.
3. Выполняют служебные процедуры обработки прерываний, ассоциирован-
ассоциированные со всеми устройствами, использующими данную IRQ-линию.
4. Заканчивают выполнение, совершая переход по адресу функции retf rom_
intr().
Для представления как состояния IRQ-линий, так и функций, выполняемых
при возникновении прерывания, требуется несколько дескрипторов. На
рис. 4.4 схематически представлены аппаратные устройства и программные
функции, применяемые для обработки прерываний. Эти функции обсужда-
обсуждаются в последующих разделах этой главы.
Рис. 4.4. Обработка прерываний ввода/вывода
Векторы прерываний
Как видно из табл. 4.2, физическим прерываниям можно присвоить любой
вектор в диапазоне от 32 до 238. Однако вектор 128 используется в Linux для
реализации системных вызовов.
IBM-совместимая архитектура персональных компьютеров требует, чтобы
некоторые устройства были статически привязаны к конкретным IRQ-
линиям. В частности:
□ таймер интервалов должен быть соединен с линией IRQ 0 (см. главу 6);
□ подчиненный программируемый контроллер прерываний 8259А должен
быть соединен с линией IRQ 2 (хотя в настоящее время применяются бо-
лее совершенные программируемые контроллеры прерываний, Linux по-
прежнему поддерживает старые контроллеры 8259А);
□ внешний математический сопроцессор должен быть соединен с линией
IRQ 13 (хотя новейшие процессоры 80x86 уже не пользуются таким уст-
устройством, Linux продолжает поддерживать надежную модель 80386);
П вообще, устройство ввода/вывода может быть соединено с ограниченным
набором IRQ-линий. (Фактически, работая на старом персональном ком-
компьютере, на котором совместное использование линий IRQ невозможно,
вы не можете установить новую карту из-за IRQ-конфликтов с уже имею-
имеющимися аппаратными устройствами.)
Таблица 4.2. Векторы прерываний в Linux
Диапазон векторов Применение
0-19 @x0-0x13) Немаскируемые прерывания и исключения
20-31 @xl4-0xlf) Зарезервировано компанией Intel
32-127 @x20-0x7f) Внешние прерывания (запросы IRQ)
128 @x80) Программное исключение для системных вызовов (см. главуЮ)
129-238 @х81-0хее) Внешние прерывания (запросы IRQ)
239 (Oxef) Прерывание по таймеру от локального программируемого кон-
контроллера прерываний (см. главу 6)
240 (Oxf 0) Термальное прерывание от локального программируемого кон-
контроллера прерываний (введено в моделях Pentium 4)
241-250 @xfl-0xfa) Зарезервировано в Linux для будущего использования
251-253 @xfb-0xfd) Межпроцессорные прерывания (см. разд. "Обработка межпро-
межпроцессорных прерываний" далее в этой главе)
254 (Oxfе) Прерывание по ошибке от локального программируемого кон-
контроллера прерываний (генерируется, когда этот контроллер
распознает ошибочную ситуацию)
255 (Oxf f) Сфабрикованное прерывание от локального программируемого
контроллера прерываний (генерируется, если процессор мас-
маскирует прерывание, когда аппаратное устройство возбуждает
, его)
Существует три способа выбрать линию для устройства с конфигурируемым
IRQ:
□ с помощью аппаратных перемычек (только на очень старых картах);
□ с помощью утилиты, поставляемой с устройством и выполняемой при его
установке. Такая программа либо просит пользователя выбрать доступную
IRQ-линию, либо зондирует систему и определяет свободную линию сама;
□ с помощью аппаратного протокола, выполняемого при запуске системы.
Периферийные устройства объявляют, какие линии они готовы использо-
использовать, а окончательные значения являются предметом переговоров, имею-
имеющих целью минимизацию конфликтов. Когда эта процедура закончена,
каждый обработчик прерываний может считать назначенный номер IRQ с
помощью функции, обращающейся к определенным портам ввода/вывода
на устройстве. Например, драйверы устройств, удовлетворяющих стан-
стандарту PCI (Peripheral Component Interconnect, Взаимосвязь периферий-
периферийных компонентов), используют группу функций, таких как pci_read_
conf igbyte (), для доступа к пространству конфигурации устройства.
В табл. 4.3 приведена достаточно произвольная схема организации устройств
и IRQ-линий на типичном персональном компьютере.
Таблица 4.3. Пример присваивания IRQ-линий устройства ввода/вывода
IRQ INT Аппаратное устройство
0 32 Таймер
1 33 Клавиатура
2 34 Каскад программируемых контроллеров прерываний
3 35 Второй последовательный порт
4 36 Первый последовательный порт
6 38 Гибкий диск
8 40 Системные часы
10 42 Сетевой интерфейс
11 43 USB-порт, звуковая карта
12 44 Мышь PS/2
13 45 Математический сопроцессор
14 46 Первая цепь контроллера ЕЮЕ-диска
15 47 Вторая цепь контроллера ЕЮЕ-диска
Ядро должно распознать, какое устройство ввода/вывода соответствует каж-
каждой IRQ-линии до включения прерываний. В противном случае — как оно,
например, будет обрабатывать сигнал от SCSI-диска, не зная, какой вектор
соответствует этому устройству? Соответствие устанавливается при инициа-
инициализации драйвера каждого устройства (см. главу 13).
Структуры данных для IRQ-линий
Как всегда, при обсуждении сложных операций, включающих в себя перехо-
переходы из одного состояния в другое, полезно разобраться, где хранятся ключе-
ключевые данные. Итак, в этом разделе описываются структуры данных, поддер-
поддерживающие обработку прерываний, а также их расположение в различных де-
дескрипторах. На рис. 4.5 схематически изображено соотношение между
основными дескрипторами, представляющими состояние IRQ-линий. (На ри-
рисунке не показаны структуры, необходимые при обработке softirq-функций и
тасклетов; они обсуждаются далее в этой главе.)
Рис. 4.5. Дескрипторы IRQ-линий
У каждого вектора прерывания есть свой дескриптор irqdesct, поля кото-
которого приводятся в табл. 4.4. Все эти дескрипторы собраны в массив irqdesc.
Таблица 4.4. Дескриптор irq_desc_t
Поле Описание
handler Указывает на PIC-объект (дескриптор hw_irq_controller), который
обслуживает IRQ-линию
handler_data Указатель на данные, используемые методами Р1С-объекта
action Идентифицирует служебные процедуры обработки прерываний, кото-
которые должны быть вызваны, когда возникнет прерывание. Это поле
указывает на первый элемент списка дескрипторов irqaction, ассо-
ассоциированных с прерыванием. Дескриптор irqaction описан далее в
этой главе
status Набор флагов, описывающих состояние IRQ-линии (см. табл. 4.5)
depth Содержит 0, если IRQ-линия включена, и положительное значение,
если она была отключена хотя бы раз
Таблица 4.4 (окончание)
Поле Описание
irq_count Счетчик числа прерываний на IRQ-линии (только для диагностики)
irqs_unhandled Счетчик числа необработанных прерываний на IRQ-линии (только для
диагностики)
lock Спин-блокировка, используемая для сериализации обращений к де-
дескриптору IRQ-линии и к PIC-объекту (см. главу 5)
Прерывание называется неожиданным, если оно не обрабатывается ядром,
т. е. если либо с IRQ-линией не ассоциирована никакая служебная процедура
обработки прерываний, либо ни одна из служебных процедур обработки пре-
прерываний, ассоциированных с этой линией, не распознает прерывание как воз-
возбужденное ее аппаратным устройством. Обычно ядро проверяет количество
неожиданных прерываний, принятых по IRQ-линии, чтобы отключить ее, ес-
если неисправное аппаратное устройство снова и снова возбуждает прерыва-
прерывание. Поскольку IRQ-линию могут совместно использовать несколько уст-
устройств, ядро не отключает ее сразу после обнаружения одного необработан-
необработанного прерывания. Вместо этого оно сохраняет в полях irqcount и irqs_
unhandied дескриптора irqdesct суммарное количество прерываний и коли-
количество неожиданных прерываний соответственно. Когда возникает 100-ты-
100-тысячное прерывание, ядро отключает линию, если количество необработанных
прерываний превышает 99 900 (то есть если меньше 101 прерывания из
100 000 принятых оказались ожидаемыми прерываниями от аппаратных уст-
устройств, не использующих эту линию).
Состояние IRQ-линии описывается флагами, перечисленными в табл. 4.5.
Таблица 4.5. Флаги, описывающие состояние IRQ-линии
Имя флага Описание
irq_inprogress Выполняется обработчик прерывания
irq_disabled Линия IRQ намеренно отключена драйвером устройства
irq_pending На линии возник запрос на прерывание; его подтверждение было
отправлено программируемому контроллеру прерываний, но сам
запрос еще не был обработан ядром
irq_replay Линия IRQ была отключена, но подтверждение предыдущего за-
запроса на прерывание еще не отправлено программируемому кон-
контроллеру прерываний
irq_autodetect Ядро пользуется IRQ-линией, опрашивая аппаратное устройство
irq_waiting Ядро пользуется IRQ-линией, опрашивая аппаратное устройство,
причем соответствующее прерывание еще не было возбуждено
Таблица 4.5 (окончание)
Имя флага Описание
irq_level В архитектуре 80x86 не используется
irq_masked He используется
irqpercpu В архитектуре 80*86 не используется
Поле depth и флаг irqdisabled дескриптора irqdesct определяют, включе-
включена или отключена IRQ-линия. Всякий раз, когда вызывается функция
disable_irq() ИЛИ disable_irq_nosync (), значение В ПОЛе depth увеличивает-
ся. Если depth равно 0, функция отключает IRQ-линию и устанавливает для
Нее флаг IRQDISABLED8. И Наоборот, КаЖДЫЙ ВЫЗОВ фуНКЦИИ enable_irq()
уменьшает значение поля. Когда поле depth становится равным 0, функция
включает IRQ-линию и сбрасывает флаг irqdisabled.
На этапе инициализации системы функция initiRQO устанавливает поле
status у всех главных дескрипторов IRQ-линий в значение irqdisabled.
Кроме того, функция initiRQ () обновляет таблицу дескрипторов прерыва-
прерываний, заменяя шлюзы прерываний, установленные функцией setupidt () (см.
разд. "Инициализация таблицы дескрипторов прерываний"ранее в этой гла-
главе), на новые. Это делается с помощью следующих операторов:
for (i = 0; i < NR_IRQS; i++)
if (i+32 != 128)
set_intr_gate(i+32,interrupt[i]);
Этот код просматривает массив interrupt в поисках адресов обработчиков
прерываний, которые он использует при установке шлюзов прерываний.
Каждый элемент п массива interrupt хранит адрес обработчика прерываний
по линии IRQ n (см. разд. "Сохранение регистров для обработчика прерыва-
прерываний" далее в этой главе). Обратите внимание, что шлюз прерываний, соответ-
соответствующий вектору 128, остается нетронутым, потому что он используется
для программного исключения, генерируемого системным вызовом.
Кроме чипа 82 5 9А, упомянутого в начале главы, Linux поддерживает не-
несколько других схем программируемых контроллеров прерываний, например,
SMP IO-APIC, внутренний контроллер 8259 для Intel PIIX4 и контроллер SGI
Visual Workstation Cobalt (IO-)APIC. Чтобы унифицировать работу с такими
8 В отличие от функции disable_irq_nosync (), функция disable_irq (п) ждет, пока все обра-
обработчики прерываний для IRQ n, работающих на других процессорах, завершат выполнение, и только
после этого она возвращает управление.
устройствами, Linux использует PIC-объект, состоящий из имени програм-
программируемого контроллера прерываний и семи стандартных методов такого кон-
контроллера. Преимущество подобного объектно-ориентированного подхода в
том, что драйверам не нужно знать, какой программируемый контроллер
прерываний установлен в системе. Каждый источник прерываний, видный
драйверу, прозрачно связан с соответствующим контроллером. Структура
данных, определяющая PIC-объект, называется hwinterrupttype (а также
hw_irq_controller).
Ради конкретности изложения предположим, что у нашего компьютера один
процессор и два программируемых контроллера прерываний 8259А, обеспе-
обеспечивающих 16 стандартных IRQ-линий. В этом случае поля handler всех ше-
шестнадцати дескрипторов irqdesct указывают на переменную типа
i8259A_irq_type, описывающую контроллер 8259А. Эта переменная инициа-
инициализируется следующим образом:
struct hw_interrupt_type i8259A_irq_type = {
.typename = "XT-PIC",
.startup = startup_8259A_irq,
.shutdown = shutdown_8259A_irq,
.enable = enable_8259A_irq,
.disable = disable_8259A_irq,
.ack = mask_and_ack_8259A,
.end = end_8259A_irq,
.set_affinity = NULL
};
Первое поле этой структуры, "xt-pic", является именем программируемого
контроллера прерываний. Далее идут указатели на шесть различных функ-
функций, используемых при программировании контроллера. Первые две функ-
функции соответственно открывают и закрывают IRQ-линию чипа. Для чипа
8259А они совпадают с третьей и четвертой функциями, которые включают и
выключают линию. Функция mask_and_ack_8259A() подтверждает получение
IRQ-запроса, посылая соответствующие байты на порты ввода/вывода 82 5 9А.
Функция end_8259A_irq () вызывается, когда обработчик прерывания для IRQ-
линии завершает работу. Метод setaff inity установлен в null. Он исполь-
используется в многопроцессорных системах для объявления "близости" процессо-
процессоров к указанным IRQ-линиям, т. е. для определения, какие процессоры долж-
должны работать с той или иной IRQ-линией.
Как было сказано ранее, одну IRQ-линию могут использовать несколько уст-
устройств. Поэтому ядро поддерживает дескрипторы irqaction (см. рис. 4.5),
каждый из которых ссылается на конкретное аппаратное устройство и кон-
кретное прерывание. Поля такого дескриптора перечислены в табл. 4.6, а
флаги — в табл. 4.7.
Таблица 4.6. Поля дескриптора irqaction
Имя поля Описание
handler Указывает на служебную процедуру обработки прерываний для устройства
ввода/вывода. Это главное поле, позволяющее нескольким устройствам
использовать одну IRQ-линию
flags Это поле включает в себя несколько полей, описывающих связь между
IRQ-линией и устройством ввода/вывода (см. табл. 4.7)
mask He используется
name Имя устройства ввода/вывода (выводится при распечатке обслуживаемых
линий IRQ из файла /proc/interrupts)
devid Закрытое поле для устройства ввода/вывода. Как правило, оно идентифи-
идентифицирует само устройство ввода/вывода (например, оно может содержать
его старший и младший номера; см. главу 13) или указывает на данные
драйвера устройства
next Указывает на следующий элемент в списке дескрипторов irqaction. Эле-
Элементы списка ссылаются на аппаратные устройства, использующие одну
IRQ-линию
irq IRQ-линия
dir Указывает на дескриптор каталога /proc/irq/n, ассоциированного с линией
IRQn
Таблица 4.7. Флаги дескриптора Lrqaction
Имя флага Исключение
sa_interrupt Обработчик должен выполняться при отключенных прерываниях
sa_shirq Устройство позволяет другим устройствам использовать его
IRQ-линию
sa_sample_random Устройство можно считать источником случайных событий, и его
может использовать генератор случайных чисел ядра. (Этой
функциональной возможностью можно воспользоваться, считы-
считывая случайные числа из файлов устройств /dev/random и
/dev/urandom)
И наконец, массив irqstat содержит nrcpus элементов, по одному на каж-
каждый процессор в системе. Каждый элемент имеет тип irqcpustatt и содер-
содержит несколько счетчиков и флагов, используемых ядром для отслеживания
того, чем занимаются процессоры в настоящий момент (табл. 4.8).
Таблица 4.8. Поля структуры Lrq_cpustat_t
Имя поля Описание
softirq_pending Набор флагов, обозначающих softirq-функции, ожидающие вы-
выполнения
idle_timestamp Момент времени, когда процессор освободился (имеет смысл,
только если процессор сейчас свободен)
nmi_count Количество NMI-прерываний
apic_timer_irqs Количество прерываний по таймеру локального усовершенство-
усовершенствованного программируемого контроллера прерываний (см. главу 6)
Распределение IRQ в многопроцессорных системах
Linux придерживается модели симметричной многопроцессорной обработки
SMP (Symmetric Multiprocessing). Это, в первую очередь, означает, что ядро
не должно оказывать предпочтение одному процессору за счет других. По-
Поэтому оно пытается распределить сигналы IRQ, поступающие от аппаратных
устройств, среди всех процессоров по круговому принципу. В результате
процессоры расходуют примерно одинаковую долю своего рабочего времени
на обслуживание прерываний ввода/вывода.
Ранее в этой главе мы говорили, что в многоконтроллерной APIC-системе
применяются довольно сложные механизмы динамического распределения
IRQ-сигналов среди процессоров.
На этапе начальной загрузки системы загружающий процессор выполняет
функцию setupiOAPicirqs () для инициализации чипа I/O APIC. Двадцать
четыре записи таблицы переадресации прерываний этого чипа заполняются
так, чтобы IRQ-сигналы от устройств ввода/вывода могли быть направлены
любому процессору в системе в соответствии с принципом "наименьшего
приоритета". Кроме того, на этапе начальной загрузки все процессоры вы-
выполняют функцию setupiocaiAPic (), ответственную за инициализацию ло-
локальных APIC-контроллеров. В частности, регистр приоритета задачи (ре-
(регистр TPR) каждого чипа инициализируется некоторым фиксированным
значением, свидетельствующим о том, что процессор готов обрабатывать
IRQ-сигналы любого типа, независимо от приоритета. Ядро Linux не
модифицирует это значение после своей инициализации.
Все регистры приоритетов задач содержат одинаковые значения, поэтому все
процессоры имеют одинаковый приоритет. Для выхода из тупиковых ситуа-
ситуаций многоконтроллерная APIC-система использует значения в регистрах ар-
арбитражных приоритетов локальных APIC-контроллеров, о чем уже было ска-
зано ранее. Поскольку такие значения автоматически меняются после каждо-
каждого прерывания, IRQ-сигналы в большинстве случаев справедливо распреде-
распределяются среди всех процессоров9.
Короче говоря, когда аппаратное устройство возбуждает IRQ-сигнал, много-
многоконтроллерная APIC-система выбирает один из процессоров и доставляет
сигнал соответствующему локальному APIC-контроллеру, который, в свою
очередь, прерывает свой процессор. Остальные процессоры не уведомляются
об этом событии.
Все это, как по волшебству, выполняется аппаратной частью, и ядру не нуж-
нужно ни о чем заботиться после инициализации многоконтроллерной APIC-
системы. К сожалению, в некоторых случаях аппаратуре не удается распре-
распределить прерывания равномерно по всем микропроцессорам (например, такая
проблема возникает у некоторых многопроцессорных материнских плат на
базе Pentium 4). Поэтому в Linux 2.6 имеется специальный поток ядра kirqd,
чтобы подкорректировать автоматическое распределение IRQ-запросов среди
процессоров, когда это необходимо.
Этот поток ядра использует одну интересную функциональную особенность
многоконтроллерных APIC-систем, называемую IRQ-близостью (афинно-
стью) процессора: модифицируя записи таблицы переадресации прерываний
APIC-контроллера ввода/вывода, можно направить сигнал прерывания на
конкретный процессор. Для этого следует вызвать функцию set_ioapic_
affinityirqo, которая принимает два параметра: вектор прерывания, кото-
которое следует переадресовать, и 32-битовую маску, определяющую процессо-
процессоры, которые готовы принять прерывание. IRQ-близость может быть изменена
системным администратором, для чего он должен записать новую битовую
маску процессоров в файл /proc/irq/n/smpaffinity (где п— вектор преры-
прерывания).
Поток ядра kirqd периодически вызывает функцию doirqbaiance о, которая
следит за количеством прерываний, принятых каждым процессором за по-
последнее время. Если функция обнаруживает значительный дисбаланс по IRQ
между наиболее и наименее загруженными процессорами, она либо выбира-
выбирает IRQ-запрос для переноса его с одного процессора на другой, либо
"разбрасывает" все запросы по всем процессорам.
9 Впрочем, из этого правила существует исключение. Linux обычно настраивает локальные APIC-
контроллеры таким образом, чтобы выделить фокусный процессор, если таковой существует. Фо-
Фокусный процессор будет ловить IRQ-запросы одного типа при условии, что он ранее принял запрос
этого типа и еще не закончил выполнять обработчик прерываний. Заметим, однако, что компания
Intel прекратила поддержку фокусных процессоров в Pentium 4.
Стеки режима ядра
Как было сказано в главе 3, дескриптор threadinf о каждого процесса связан
со стеком режима ядра в структуре threadunion, состоящей из одного или
двух страничных кадров, в зависимости от опции, выбранной при компиля-
компиляции ядра. Если размер структуры threadunion равен 8 Кбайт, стек режима
ядра текущего процесса используется для управляющих трактов ядра всех
типов: исключений, прерываний и функций отложенного выполнения (см.
разд. "Softirq-функции и тасклеты" далее в этой главе). Если же размер
структуры threadunion равен 4 Кбайт, ядро использует стеки режима ядра
трех типов:
□ стек исключений — используется при обработке исключений (в том числе
системных вызовов). Это стек, содержащийся в структуре threadunion,
которая есть у каждого процесса. Таким образом, ядро работает с отдель-
отдельным стеком исключений для каждого процесса в системе;
□ стек "жестких" IRQ-запросов — используется при обработке прерываний.
У каждого процессора в системе имеется один стек жестких IRQ-запросов,
и каждый такой стек хранится в одном страничном кадре;
□ стек "мягких" IRQ-запросов — используется при обработке функций от-
отложенного выполнения (softirq-функций или тасклетов). У каждого про-
процессора в системе имеется один стек мягких IRQ-запросов, и каждый та-
такой стек хранится в одном страничном кадре.
Все стеки жестких IRQ-запросов хранятся в массиве hardirqstack, а все сте-
стеки мягких IRQ-запросов— в массиве softirqstack. Каждый элемент этих
массивов является объединением, имеющим тип irqctx, и занимает одну
страницу. В нижней части этой страницы находится структура threadinf о, а
свободная память отведена под стек (вспомним, что каждый стек растет в на-
направлении уменьшения адресов). Таким образом стеки жестких и гибких
IRQ-запросов во многом похожи на стеки исключений, описанные в главе 3.
Единственное отличие состоит в том, что структура threadinfo, спаренная
со стеком, ассоциирована с процессором, а не процессом.
Массивы hardirqctx и softirqctx позволяют ядру быстро определить стек
жестких и, соответственно, стек мягких IRQ-запросов для данного процессо-
процессора, т. к. эти массивы содержат ссылки на соответствующие элементы irqctx.
Сохранение регистров для обработчика прерываний
Когда процессор принимает сигнал прерывания, он приступает к выполне-
выполнению кода, расположенного по адресу, хранящемуся в соответствующем шлю-
шлюзе таблицы дескрипторов прерываний.
Как и в других случаях переключения контекста, необходимость сохранять
регистры усложняет жизнь разработчику ядра, поскольку кодировать сохра-
сохранение и восстановление регистров приходится на ассемблере. Однако по ходу
этих операций ожидается, что процессор вызовет функцию на языке С.
В этом разделе мы опишем ассемблерную часть работы с регистрами, а в сле-
следующем — некоторые трюки, применяемые в коде функции на языке С, ко-
которая вызывается следом.
Сохранение регистров является первоочередной задачей обработчика преры-
прерываний. Как было сказано ранее, адрес обработчика прерывания IRQ n изна-
изначально хранится в элементе массива interrupt [n], а затем копируется в шлюз
прерываний, хранящийся в соответствующей записи таблицы дескрипторов
прерываний.
Массив interrupt строится с помощью нескольких ассемблерных инструкций
в файле arch/i386/kernel/entry.S. Массив состоит из nrirqs элементов, где
макрос nrirqs возвращает либо число 224, если ядро поддерживает чип I/O
APIC10, либо число 16, если ядро работает с чипами программируемых кон-
контроллеров прерываний 8259А. Элемент массива с индексом п хранит адрес
следующего фрагмента ассемблерного кода:
pushl $n-256
jmp coramon_interrupt
В результате в стек будет записан номер IRQ, ассоциированный с прерывани-
прерыванием и уменьшенный на 256. Ядро представляет все IRQ-запросы в виде отри-
отрицательных чисел, потому что оно резервирует положительные номера преры-
прерываний для идентификации системных вызовов (см. главу 10). После этого для
всех обработчиков прерываний можно выполнять один и тот же код, ссыла-
ссылаясь на это число. Общий код начинается с метки commoninterrupt и состоит
из следующих макросов и инструкций:
coramon_interrupt:
SAVE_ALL
movl %esp,%eax
call do_IRQ
jmp ret_from_intr
Макрос saveall разворачивается в следующий фрагмент кода:
eld
push %es
10 256 векторов — это конструктивное ограничение в архитектуре 80x86. Из них 32 используются
процессором или зарезервированы для него, и доступное пространство векторов содержит
224 вектора.
push %ds
pushl %eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
pushl %ecx
pushl %ebx
movl $ USER_DS,%edx
movl %edx,%ds
movl %edx,%es
Макрос saveall сохраняет в стеке все регистры процессора, которые могут
быть использованы обработчиком прерываний, кроме регистров efiags, cs,
eip, ss и esp, которые уже сохранены автоматически управляющим блоком.
Затем макрос загружает селектор сегмента пользовательских данных в реги-
регистры ds И es.
После сохранения регистров адрес вершины стека сохраняется в регистре еах,
а затем обработчик прерываний вызывает функцию doiRQ (). Когда выполня-
выполняется инструкция ret в функции doiRQ () (то есть когда функция завершает
работу), управление передается функции retf romintr () (см. разд. "Возврат
из прерываний и исключений" далее в этой главе).
Функция doJRQQ
Функция doiRQO вызывается для выполнения всех служебных процедур,
ассоциированных с данным прерыванием. Она объявлена следующим об-
образом:
attribute ((regparmC))) unsigned int do_IRQ(struct pt_regs *regs)
Ключевое слово regparm предписывает функции прочитать содержимое реги-
регистра еах, чтобы найти значение аргумента regs. Как мы видели ранее, регистр
еах указывает на элемент стека, содержащий значение последнего регистра,
помещенного в стек макросом saveall.
Функция doiRQ () выполняет следующие действия:
1. Выполняет макрос irqenterO, который увеличивает счетчик, отражаю-
отражающий количество вложенных обработчиков прерываний. Счетчик хранится
в поле preemptcount структуры threadinfo текущего процесса (см.
табл. 4.10).
2. Если размер структуры threadunion равен 4 Кбайт, функция переключа-
переключается на стек жестких IRQ-запросов. В частности, она выполняет следую-
следующие действия:
• вызывает функцию currentthreadinf о (), чтобы получить адрес деск-
дескриптора threadinf о, ассоциированного со стеком режима ядра, адрес
которого хранится в регистре esp (см. главу 3);
• сравнивает адрес дескриптора threadinf о, полученный на предыдущем
шаге, с адресом, хранящимся в элементе массива hardirq_
ctx[smp_processor_id() ], Т. е. С адресом дескриптора thread_info, аССО-
циированного с локальным процессором. Если адреса совпадают, зна-
значит, ядро уже использует стек жестких IRQ-запросов, и функция пере-
переходит к шагу 3. Это бывает, когда прерывание возбуждается, пока ядро
все еще обрабатывает предыдущее прерывание;
• на этом шаге происходит переключение на другой стек режима ядра.
Функция сохраняет указатель на дескриптор текущего процесса в поле
task дескриптора threadinfo в объединении irqctx локального про-
процессора. Это делается для того, чтобы макрос current вел себя, как
ожидается, когда ядро работает со стеком жестких IRQ-запросов (см.
главу 3)\
• сохраняет текущее значение регистра указателя стека esp в поле
previousesp дескриптора threadinfo В объединении irqctx ЛОКальнО-
го процессора (это поле используется только при подготовке дерева вы-
вызовов функций для дампа "kernel oops");
• загружает в регистр esp адрес вершины стека жестких IRQ-запро-
IRQ-запросов локального процессора (значение в элементе hardirq_ctx[smp_
processorido ], увеличенное на 4096). Предыдущее значение регистра
esp сохраняется в регистре ebx).
3. Вызывает функцию doiRQ о, передавая ей в качестве параметров указа-
указатель regs И НОМер IRQ, прочитанный ИЗ ПОЛЯ regs->orig_eax (CM. СЛедуЮ-
щий раздел).
4. Если на шаге 2 было фактически произведено переключение на стек жест-
жестких IRQ-запросов, функция копирует оригинальный указатель стека из ре-
регистра ebx в регистр esp, тем самым переключаясь обратно на стек исклю-
исключений или стек мягких IRQ-запросов, который использовался до переклю-
переключения.
5. Выполняет функцию irqexito, которая уменьшает счетчик прерываний
и проверяет наличие функций отложенного выполнения, которые ждут
своей очереди (см. разд. "Softirq-функцгш и тасклеты " далее в этой главе).
6. Завершает свою работу: передает управление функции retfromintro
(см. разд. "Возврат из прерываний и исключений" далее в этой главе).
Функция doJRQO
Функция doiRQ () принимает в качестве параметров номер IRQ (через ре-
регистр еах) и указатель на структуру ptregs, в которой было сохранено со-
содержимое регистров режима пользователя (через регистр edx).
Код функции эквивалентен следующему фрагменту:
spin_lock(&(irq_desc[irq].lock));
irq_desc[irq].handler->ack(irq);
irq_desc[irq].status &= ~(IRQ_REPLAY | IRQ_WAITING);
irq_desc[irq].status |= IRQ_PENDING;
if (!(irq_desc[irq].status & (IRQ_DISABLED | IRQ_INPROGRESS))
&& irq_desc[irq].action) {
irq_desc[irq].status |= IRQ_INPROGRESS;
do {
irq_desc[irq].status &= ~IRQ_PENDING;
spin_unlock(&(irq_desc[irq].lock));
handle_IRQ_event(irq, regs, irq_desc[irq].action);
spin_lock(&(irq_desc[irq].lock));
} while (irq_desc[irq].status & IRQ_PENDING);
irq_desc[irq].status &= ~IRQ_INPROGRESS;
}
irq_desc[irq].handler->end(irq);
spin_unlock(&(irq_desc[irq].lock));
Прежде чем обратиться к главному дескриптору прерывания, ядро получает
соответствующую спин-блокировку. В главе 5 мы увидим, что спин-
блокировка защищает данные от попыток одновременного обращения со сто-
стороны разных процессоров. Спин-блокировка необходима в многопроцессор-
многопроцессорной системе, потому что могут возникнуть другие прерывания того же типа,
и другие процессоры могут приступить к их обработке. Без спин-блокировки
к главному дескриптору прерывания могли бы обратиться сразу несколько
процессоров. Как мы увидим, избежать этого абсолютно необходимо.
Получив спин-блокировку, функция вызывает метод аск главного дескрипто-
дескриптора прерывания. При работе со старым программируемым контроллером пре-
прерываний 8259А соответствующая функция mask_and_ack_8259A() подтвержда-
ет прерывание и отключает IRQ-линию. Маскировка IRQ-линии гарантирует,
что процессор не станет принимать последующие прерывания этого типа,
пока не завершится обработчик прерывания. Вспомним, что функция
doiRQ () работает при отключенных внешних прерываниях. Управляющий
блок процессора автоматически сбрасывает флаг if в регистре еflags, потому
что обработчик прерывания вызывается с помощью шлюза прерывания, хра-
хранящегося в таблице дескрипторов прерываний. Однако мы вскоре увидим,
что ядро может снова включить локальные прерывания перед выполнением
служебных процедур обработки данного прерывания.
Ситуация намного усложняется при использовании усовершенствованного
программируемого контроллера ввода/вывода прерываний. В зависимости от
типа прерывания его подтверждение может быть либо произведено с по-
помощью метода аск, либо отложено до завершения обработчика прерываний
(то есть подтверждение может быть выполнено методом end). В любом слу-
случае мы принимаем как само собой разумеющееся то, что локальный APIC-
контроллер не будет воспринимать последующие прерывания этого типа, по-
пока не завершится обработчик прерываний, хотя эти прерывания могут быть
приняты другими процессорами.
Затем функция doiRQ () инициализирует несколько флагов главного деск-
дескриптора прерывания. Она устанавливает флаг irqpending, потому что пре-
прерывание было подтверждено (в определенном смысле), но фактически еще не
обработано. Кроме того, функция сбрасывает флаги irqwaiting и irqreplay
(но сейчас они нас не интересуют).
Далее функция doiRQ () проверяет, действительно ли она должна обрабо-
обработать прерывание. Существует три случая, в которых ничего не нужно пред-
предпринимать:
□ Флаг irqdisabled установлен — процессор, возможно, вызвал функцию
doiRQ (), хотя соответствующая IRQ-линия отключена. Описание этого
неочевидного случая дано в разд. "Восстановление потерянного прерыва-
прерывания" далее в этой главе. Кроме того, материнские платы с дефектами мо-
могут генерировать ложные прерывания, даже если IRQ-линия в программи-
программируемом контроллере прерываний отключена.
□ Флаг irqinprogress установлен — в многопроцессорной системе другой
процессор, возможно, обрабатывает предыдущее возникновение того же
прерывания. Почему бы не переложить обработку данного прерывания на
тот процессор? Именно так и поступает Linux. Этот подход упрощает ар-
архитектуру ядра, потому что служебным процедурам обработки прерыва-
прерываний от драйверов устройств не нужно быть реентерабельными (их выпол-
выполнение сериализуется). Кроме того, освободившийся процессор может бы-
быстро вернуться к своим прежним делам, не загрязняя свой аппаратный
кэш, а это способствует повышению производительности системы. Флаг
irqinprogress устанавливается, когда процессор собирается выполнить
служебные процедуры обработки прерывания, и поэтому функция
doiRQ () проверяет его до начала реальной работы.
□ Поле irqdesc [irq] .action содержит null — эта ситуация возникает, когда
с прерыванием не ассоциировано никакой служебной процедуры. Обычно
это случается, только когда ядро зондирует аппаратное устройство.
Предположим, что ни один из трех случаев не имеет места, и прерыва-
прерывание должно быть обслужено. Функция doiRQO устанавливает флаг
irqinprogress и входит в цикл. На каждом шаге цикла она сбрасывает флаг
irqpending, освобождает спин-блокировку прерывания и выполняет служеб-
служебные процедуры обработки прерывания, для чего вызывает функцию
handieiRQevent () (описанную далее в этой главе). Когда последняя возвра-
возвратит управление, функция doiRQ () снова получает спин-блокировку и про-
проверяет состояние флага irqpending. Если он сброшен, значит, последующие
сигналы этого прерывания не были доставлены другому процессору, так что
цикл завершается. Если же флаг irqpending установлен, значит, другой про-
процессор вызвал функцию doiRQ () для этого типа прерывания, пока этот про-
процессор ВЫПОЛНЯЛ фуНКЦИЮ handieiRQevent () . Тогда фуНКЦИЯ doiRQ () ВЫ-
полняет еще один шаг цикла, обслуживая новое возникновение этого преры-
прерывания11.
Теперь наша функция doiRQ () готова завершить свою работу, либо потому
что она уже выполнила служебные процедуры обработки прерывания, либо
потому что ей ничего не надо было делать. Функция вызывает метод end
главного дескриптора прерывания. При работе со старым программируемым
контроллером прерываний 8259А соответствующая функция end_8259A_irq()
снова включает IRQ-линию (если прерывание не было ложным). При исполь-
использовании усовершенствованного программируемого контроллера вво-
ввода/вывода прерываний метод end подтверждает прерывание (если этого не
сделал метод аск).
Наконец, функция doiRQO освобождает спин-блокировку— самая труд-
трудная часть работы закончена!
Восстановление потерянного прерывания
Функция doiRQ (), несмотря на малый объем и простоту, корректно рабо-
работает в большинстве случаев. В самом деле, флаги irqpending, irqinprogress
и irqdisabled гарантируют надлежащую обработку прерываний, даже если
имеют место аппаратные неполадки. Тем не менее в многопроцессорной сис-
системе иногда не все протекает столь гладко.
Предположим, у некоторого процессора включена IRQ-линия. Аппаратное
устройство генерирует прерывание, и многоконтроллерная APIC-система вы-
выбирает наш процессор для его обработки. Прежде чем процессор подтвердит
прерывание, IRQ-линию маскирует другой процессор. В результате, флаг
11 Поскольку IRQ_PENDING является флагом, а не счетчиком, можно обнаружить только второе
возникновение прерывания. Последующие его возникновения на каждом шаге цикла функции
do_IRQ () просто теряются.
irqdisabled оказывается установленным. В следующий момент наш процес-
процессор начинает обработку "висящего" прерывания. Функция doiRQO подтвер-
подтверждает прерывание и возвращает управление, не выполнив процедуры обра-
обработки прерываний, потому что обнаружила установленный флаг irqdisabled.
Следовательно, хотя прерывание и случилось до отключения линии IRQ, оно
теряется.
Чтобы справиться с такой ситуацией, функция enabieirqo, которую ядро
вызывает для включения IRQ-линии, вначале проверяет, не потеряно ли пре-
прерывание. Если это так, она заставляет аппаратную часть сгенерировать новое
появление потерянного прерывания:
spin_lock_irqsave(&(irq_desc[irq].lock), flags);
if (—irq_desc[irq].depth == 0) {
irq_desc[irq].status &= ~IRQ_DISABLED;
if (irq_desc[irq].status & (IRQ_PENDING | IRQ_REPLAY))
== IRQ_PENDING) {
irq_desc[irq].status |= IRQ_REPLAY;
hw_resend_irq(irq desc[irq].handler,irq);
}
irq_desc[irq].handler->enable(irq);
}
spin_lock_irqrestore(&(irq_desc[irq].lock), flags);
Потерю прерывания функция распознает, проверяя состояние флага
irqpending. Он всегда сбрасывается при выходе из обработчика прерываний.
Следовательно, если IRQ-линия отключена, а флаг установлен, то прерыва-
прерывание было подтверждено, но не обработано. В таком случае функция
hwresendirqo возбуждает новое прерывание. С этой целью она заставляет
локальный APIC-контроллер сгенерировать автопрерывание (см. разд. "Об-
"Обработка межпроцессорных прерываний" далее в этой главе). Роль флага
irqreplay состоит в том, чтобы обеспечить генерирование ровно одного ав-
автопрерывания. Вспомним, что функция doiRQ () сбрасывает этот флаг, ко-
когда приступает к обработке прерывания.
Служебные процедуры обработки прерываний
Как было сказано ранее, служебная процедура обрабатывает прерывание, вы-
выполняя некоторую операцию, специфичную для устройства какого-то одного
типа. Когда обработчику прерываний приходится выполнять служебные про-
процедуры, он вызывает функцию handieiRQevent (). Эта функция выполняет
следующие действия:
1. Включает локальные прерывания ассемблерной инструкцией sti, если
флаг sa_interrupt сброшен.
2. Выполняет служебную процедуру обработки данного прерывания при по-
помощи такого кода:
retval = 0;
do {
retval |= action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
В начале цикла переменная action указывает на начало списка структур
irqaction, определяющих действия, предпринимаемые после получения
прерывания (см. рис. 4.5).
3. Отключает локальные прерывания ассемблерной инструкцией cli.
4. Завершает работу, возвращая значение локальной переменной retval, т. е.
0, если ни одна служебная процедура не распознала прерывание, и 1
в противном случае.
Все служебные процедуры обработки прерываний принимают одинаковый
набор параметров (повторимся, они передаются через регистры еах, edx и есх
соответственно):
□ irq — номер прерывания;
□ devid — идентификатор устройства;
□ regs — указатель на структуру ptregs, хранящуюся в стеке (исключений)
режима ядра и содержащую регистры, сохраненные сразу после возникно-
возникновения прерывания. Структура ptregs состоит из 15 полей:
• первые девять полей содержат значения регистров, сохраненные мак-
макросом save_all;
• десятое поле, на которое ссылается поле по имени origeax, содержит
номер прерывания;
• остальные поля соответствуют значениям регистров, которые были ав-
автоматически сохранены управляющим блоком.
Первый параметр позволяет одной служебной процедуре работать с несколь-
несколькими IRQ-линиями, второй позволяет одной служебной процедуре обслужи-
обслуживать несколько устройств одного типа, а третий предоставляет служебной
процедуре доступ к контексту выполнения прерванного управляющего тракта
ядра. На практике большинство служебных процедур обработки прерываний
не пользуется этими параметрами.
Каждая служебная процедура обработки прерывания возвращает 1, если пре-
прерывание было эффективно обработано, т. е. если сигнал был послан аппарат-
аппаратным устройством, за обслуживание которого отвечает данная процедура (а не
другим устройством, использующим ту же IRQ-линию). В противном случае
служебная процедура возвращает 0. Этот код возврата позволяет ядру обно-
обновить счетчик неожиданных прерываний, упомянутый в разд. "Структуры
данных для IRQ-линий"ранее в этой главе.
Флаг sainterrupt главного дескриптора прерывания определяет, должны ли
прерывания быть включены или выключены, когда функция doiRQ () вызы-
вызывает служебную процедуру обработки прерывания. Служебной процедуре,
которая была вызвана при одном состоянии прерываний, разрешается пере-
перевести их в противоположное состояние. В однопроцессорной системе это
достигается ассемблерными инструкциями cli (запретить прерывания) и sti
(разрешить прерывания).
Структура служебной процедуры обработки прерываний зависит от характе-
характеристик обслуживаемого устройства. Мы приведем пару примеров таких про-
процедур в главах 6 и 13.
Динамическое выделение IRQ-линий
Как было сказано в разд. "Векторы прерываний", несколько векторов заре-
зарезервированы для конкретных устройств, а остальные обрабатываются дина-
динамически. Таким образом, имеется способ предоставить одну IRQ-линию не-
нескольким аппаратным устройствам, даже если они не допускают совместного
использования IRQ-линии. Решение состоит в том, чтобы сериализовать ак-
активизацию аппаратных устройств так, чтобы только одно владело IRQ-
линией в каждый момент времени.
Перед активизацией устройства, которое будет использовать IRQ-линию, со-
соответствующий драйвер вызывает функцию requestirqo. Эта функция соз-
создает новый дескриптор irqaction и инициализирует его значениями своих
параметров. Затем она вызывает функцию setupirqo, чтобы занести деск-
дескриптор в соответствующий список. Драйвер устройства отменяет операцию,
если функция setupirqo возвращает код ошибки, который обычно означает,
что линия IRQ уже используется другим устройством, не допускающим со-
совместное использование линии. Когда операция, выполняемая устройством,
заканчивается, драйвер вызывает функцию freeirqo, чтобы удалить деск-
дескриптор из списка и освободить область памяти.
Рассмотрим на простом примере, как работает эта схема. Предположим, что
некая программа пытается обратиться к файлу устройства /dev/fdO, который
соответствует первому накопителю на гибких дисках в системе12. Программа
может сделать это либо обратившись к файлу /dev/fdO непосредственно, либо
12 Накопители на гибких дисках — старые устройства; они обычно не допускают совместного ис-
использования IRQ-линий.
смонтировав на нем файловую систему. Контроллерам гибких дисков обычно
назначена линия IRQ 6. При таких исходных условиях драйвер может выдать
следующий запрос:
request_irqF, floppy_interrupt,
SA_INTERRUPT|SA_SAMPLE_RANDOM, "floppy", NULL);
Нетрудно заметить, что служебная процедура f loppyinterrupt () должна ра-
работать при отключенных прерываниях (флаг sainterrupt установлен), и со-
совместное использование IRQ-линии не допускается (флаг sashirq отсутству-
отсутствует). Установленный флаг sasamplerandom говорит о том, что обращения к
гибкому диску являются хорошим источником случайных событий, который
может быть использован генератором случайных чисел ядра. Когда операция
с гибким диском закончится (либо закончится ввод/вывод в файл /dev/fdO,
либо файловая система будет размонтирована), драйвер освобождает линию
IRQ 6:
free_irqF, NULL);
Чтобы занести дескриптор irqaction в нужный список, ядро вызывает функ-
функцию setupirqo, передавая ей в качестве параметров номер прерывания
irqnr и адрес new (адрес предварительно выделенного дескриптора
irqaction). Эта функция выполняет следующие действия:
1. Проверяет, использует ли другое устройство IRQ-линию с номером irqnr,
и, если это так, может ли эта линия быть совместно использована (о чем
говорят флаги sashirq в дескрипторах irqaction обоих устройств). Воз-
Возвращает код ошибки, если совместное использование IRQ-линии невоз-
невозможно.
2. Добавляет *new (новый дескриптор irqaction, на который указывает
параметр new) в конец списка, на который указывает переменная
irq_desc[irq_nr]->action.
3. Если никакое другое устройство не использует ту же IRQ-линию, функция
сбрасывает флаги irq_disabled, irq_autodetect, irq_waiting и
IRQINPROGRESS В ПОЛе flags Дескриптора *new И ВЫЗЫВаеТ МеТОД startup
PIC-объекта irq_desc[irq_nr]->handier, чтобы IRQ-сигналы были навер-
няка включены.
Рассмотрим пример использования функции setupirqo на этапе инициали-
инициализации системы. Ядро инициализирует дескриптор irqO таймера интервалов,
выполняя следующие инструкции в функции timeinit () (см. главу 6):
struct irqaction irqO =
{timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL};
setup_irq@, &irq0);
Вначале инициализируется переменная irqO, имеющая тип irqaction: в поле
handier записывается адрес функции timerinterrupt (), в поле flags заносит-
заносится значение флага sainterrupt, в поле name — имя "timer", а пятое поле уста-
устанавливается в null, т. к. значение devid не используется. Затем ядро вызыва-
вызывает функцию setupirqo, чтобы вставить irqO в список дескрипторов
irqaction, ассоциированных с линией IRQ 0.
Обработка межпроцессорных прерываний
Межпроцессорные прерывания позволяют одному процессору посылать сиг-
сигналы прерывания любому другому процессору в системе. Как было показано
ранее, межпроцессорное прерывание доставляется не по линии IRQ, а напря-
напрямую, в виде сообщения по шине, соединяющей локальные APIC-контроллеры
всех процессоров (это либо специально выделенная шина на старых материн-
материнских платах, либо системная шина материнских плат для Pentium 4).
В многопроцессорных системах Linux использует межпроцессорные преры-
прерывания трех типов (см. также табл. 4.2):
□ callfunctionvector (вектор Oxfb) — посылается всем процессорам, кро-
кроме отправителя, заставляя их выполнить функцию, переданную отправи-
отправителем. Соответствующий обработчик прерываний называется caii_
functioninterrupt (). Эта функция (адрес которой передается через гло-
глобальную переменную caiidata) может, например, заставить другие
процессоры прекратить работу или установить содержимое регистров
диапазона типов памяти (MTRR-регистровI3. Обычно это прерывание
отправляется всем процессорам, кроме того процессора, который
выполняет вызывающую функцию с использованием служебной функции
smp_call_function ().
П reschedulevector (вектор Oxf с) — когда процессор получает прерывание
ЭТОГО ТИПа, СООТВеТСТВуЮЩИЙ Обработчик ПО ИМеНИ reschedule_
interrupt о ограничивается подтверждением прерывания. Перепланиров-
Перепланировка задач происходит автоматически после возврата из прерывания
(см. разд. "Возврат из прерываний и исключений" далее в этой главе).
П invalidate_tlb_vector (вектор Oxfd)— посылается всем процессорам,
кроме отправителя, заставляя их объявить недействительными свои TLB-
буферы. Соответствующий обработчик ПО Имени invalidate_interrupt()
очищает некоторые записи TLB-буфера данного процессора, как описано
в главе 2.
13 Начиная с модели Pentium Pro, микропроцессоры Intel имеют эти дополнительные регистры, об-
облегчающие настройку операций с кэшем. Например, Linux может использовать эти регистры, чтобы
отключить аппаратный кэш для адресов, отображающих фрейм-буфер графической карты PCI/AGP,
оставив в силе режим "объединения операций записи", в котором блок управления страницами объ-
объединяет операции записи в большие пакеты, перед копированием данных во фрейм-буфер.
Ассемблерный код обработчиков межпроцессорных прерываний генерирует-
генерируется макросом buildinterrupt: он сохраняет регистры, помещает в стек номер
вектора, уменьшенный на 256, а затем вызывает функцию на языке С, имя
которой состоит из префикса smp_ и имени низкоуровневого обработчика
прерывания. Например, высокоуровневый обработчик межпроцессорного
прерывания callfunctionvector, вызываемый низкоуровневым обработчи-
обработчиком call_function_interrupt (), НОСИТ ИМЯ smp_call_function_interrupt ().
Каждый обработчик высокого уровня подтверждает межпроцессорное пре-
прерывание на локальном APIC-контроллере, а затем выполняет конкретные
действия, требуемые прерыванием.
Благодаря следующей группе функций, выдача межпроцессорных прерыва-
прерываний является простым делом:
□ sendipiaiio — отправляет межпроцессорное прерывание всем процес-
процессорам (включая отправителя);
□ sendiPiaiibutseif о — отправляет межпроцессорное прерывание всем
процессорам, кроме отправителя;
□ sendiPiseif о — отправляет межпроцессорное прерывание самому от-
отправителю;
□ sendipimasko — отправляет межпроцессорное прерывание группе про-
процессоров, определяемой с помощью битовой маски.
Softirq-функции и тасклеты
В разд. "Обработка прерываний" ранее в этой главе мы заметили, что неко-
некоторые задачи, стоящие перед ядром, не являются критическими, и их можно
отложить на достаточно продолжительное время, если необходимо. Вспом-
Вспомним, что служебные процедуры, вызываемые обработчиками прерываний,
сериализуются, и часто бывает необходимо, чтобы до завершения соответст-
соответствующего обработчика новые прерывания не возникали. С другой стороны,
задачи, допускающие задержку, могут быть выполнены при включенных
прерываниях. Вынос их за пределы обработчика прерываний позволяет со-
сохранить малое время реакции ядра. Это очень важное свойство с точки зре-
зрения многих приложений, критических по времени, которые ожидают, что их
запросы на прерывание будут обслужены за несколько миллисекунд.
Linux 2.6 справляется с этой проблемой с помощью двух типов прерываемых
функций ядра, так называемых функций отложенного выполнения14 (softirq-
14 Их также называют программными прерываниями, но мы будем применять термин "функции
отложенного выполнения", чтобы избежать путаницы с программными исключениями, которые в
документации Intel также называются "software interrupts" (программные прерывания).
функций и тасклетов), и функций, выполнение которых обеспечивается ра-
рабочими очередями (мы опишем их в разд. "Рабочие очереди" далее в этой
главе).
Softirq-функции и тасклеты строго взаимосвязаны, потому что вторые реали-
реализованы поверх первых. На самом деле, термин "softirq", встречающийся в ис-
исходном коде ядра, нередко обозначает оба типа функций отложенного вы-
выполнения. Другим широко используемым термином является контекст пре-
прерывания: он показывает, что в настоящий момент ядро выполняет либо
обработчик прерываний, либо функцию отложенного выполнения.
Softirq-функции выделяются статически (то есть определяются на этапе ком-
компиляции), а тасклеты могут быть также выделены и проинициализированы на
этапе выполнения (например, при загрузке модуля ядра). Softirq-функции мо-
могут выполняться параллельно на нескольких процессорах, даже если имеют
один тип. Иначе говоря, они являются реентерабельными функциями и
должны явным образом защищать свои данные с помощью спин-блокировок.
Тасклеты не беспокоятся по этому поводу, потому что их выполнение строже
контролируется ядром. Тасклеты одного типа всегда сериализованы, т. е. не
могут одновременно выполняться на разных процессорах. Что касается таск-
летов разных типов, они могут выполняться параллельно на нескольких про-
процессорах. Сериализация тасклетов облегчает жизнь разработчикам драйверов
устройств, поскольку эти функции не обязаны быть реентерабельными.
Вообще говоря, над функциями отложенного выполнения могут быть произ-
произведены четыре операции:
□ Инициализация — определяет новую функцию отложенного выполнения.
Эта операция обычно выполняется, когда ядро инициализирует само себя
или загружает модуль.
□ Активизация— помечает функцию отложенного выполнения как "вися-
"висящую", т. е. как подлежащую выполнению, когда ядро в следующий раз на-
назначит раунд выполнения таких функций. Активизация может быть про-
произведена в любой момент (даже во время обработки прерывания).
□ Маскировка — избирательно запрещает запуск функции отложенного вы-
выполнения, так что ядро не станет ее выполнять, даже если она активизиро-
активизирована. Мы увидим в разд. "Запрет и разрешение функций отложенного вы-
выполнения" главы 5, что запрет функций отложенного выполнения иногда
играет важную роль.
□ Выполнение— выполняет висящую функцию отложенного выполнения,
вместе с остальными висящими функциями того же типа. Выполнение
происходит в строго определенное время, как описано в разд. "Softirq-
функции" далее в этой главе.
Активизация и выполнение тесно связаны: функция отложенного выполне-
выполнения, которая была активизирована данным процессором, должна быть вы-
выполнена на нем же. Нет никаких достаточно очевидных причин считать, что
это правило способствует повышению производительности системы. Привя-
Привязывание функции отложенного выполнения к активизировавшему ее процес-
процессору теоретически может оптимизировать использование аппаратного кэша
этого процессора. В конце концов, не так уж невероятно, что управляющий
тракт ядра, выполнивший активизацию, обращается к тем же структурам
данных, которые будут использованы активизированной функцией. Однако
соответствующие строки вполне могут исчезнуть из кэша к моменту старта
функции, поскольку ее выполнение обычно откладывается на продолжитель-
продолжительное время. Более того, привязывание функции к процессору является потен-
потенциально "опасной" операцией, потому что, в конце концов, один процессор
может оказаться сильно загруженным, а остальные будут работать вхо-
вхолостую.
Softi rq-фу н кци и
В Linux 2.6 имеется ограниченное количество softirq-функций. В большинст-
большинстве случаев вполне подходят тасклеты, а написать их гораздо проще, потому
что они не должны быть реентерабельными.
Фактически только шесть типов softirq-функций, которые представлены в
табл. 4.9, определены в настоящее время.
Таблица 4.9. Softirq-функции, используемые в Linux 2.6
Softirq-функций {п^ортет) Описание
hi_softirq 0 Выполняет высокоприоритетные тасклеты
timer_softirq 1 Тасклеты, имеющие отношение к прерываниям
по таймеру
net_tx_softirq 2 Пересылает пакеты данных сетевым картам
net_rx_softirq 3 Принимает пакеты данных от сетевых карт
scsi_softirq 4 Обработка SCSI-команд после прерывания
tasklet_softirq 5 Выполняет обычные тасклеты
Индекс softirq-функции определяет ее приоритет: чем меньше индекс, тем
выше приоритет, потому что softirq-функции выполняются, начиная с индек-
индекса 0.
Структуры данных для softirq-функций
Основной структурой, используемой для представления softirq-функций, яв-
является массив softirqvec, состоящий из тридцати двух элементов типа
softirqaction. Приоритет softirq-функций равен индексу соответствующего
элемента sof tirqaction внутри массива. Как следует из табл. 4.9, фактиче-
фактически используются только первые шесть элементов массива. Структура
softirqaction состоит из двух полей: указателя action на softirq-функцию и
указателя data на специфическую структуру данных, которая может понадо-
понадобиться softirq-функций.
Другим чрезвычайно важным полем, используемым для отслеживания вы-
вытеснения в ядре и вложенности управляющих трактов ядра, является 32-би-
32-битовое поле preemptcount, хранящееся в поле threadinf о каждого дескрипто-
дескриптора процесса (см. главу 3). Это поле кодирует три разных счетчика и флаг, как
показано в табл. 4.10.
Таблица 4.10. Подполя поляpreempt_count
Биты Описание
0-7 Счетчик вытеснений (макс, значение 255)
8-15 Счетчик softirq-функций (макс, значение 255)
16-27 Счетчик прерываний (макс, значение 4096)
28 Флаг PREEMPT_ACTIVE
Первый счетчик показывает, сколько раз вытеснение в ядре было явно от-
отключено для локального процессора; нулевое значение говорит о том, что
вытеснение в ядре явным образом не отключалось ни разу. Второй счетчик
показывает глубину уровня отключения функций отложенного выполнения
(уровень 0 означает, что эти функции включены). Третий счетчик показывает
количество вложенных обработчиков прерываний у локального процессора
(значение увеличивается функцией irqentero и уменьшается функцией
irqexito; см. разд. "Обработка прерываний ввода/вывода" далее в этой
главе).
Название поля preemptcount оправдывает себя: вытеснение в ядре должно
быть отключено либо явным образом в коде ядра (счетчик вытеснений не ра-
равен 0), либо когда ядро работает в контексте прерывания. Таким образом,
чтобы определить, возможно ли вытеснение текущего процесса, ядро быстро
проверяет поле preemptcount на предмет нулевого значения. Вытеснение
в ядре обсуждается в разд. "Вытеснение в ядре" главы 5.
Макрос ininterrupt() проверяет счетчик прерываний и счетчик softirq-
фуНКЦИЙ В ПОЛе current_thread_info()->preempt_count. ЕСЛИ любой ИЗ ЭТИХ
двух счетчиков положителен, макрос возвращает ненулевое значение, в про-
противном случае — нулевое. Если ядро не использует несколько стеков режима
ядра, макрос обращается к полю preemptcount дескриптора threadinf о те-
текущего процесса. Если же ядро их использует, макрос может прочитать поле
preemptcount В дескрипторе threadinf о, хранящемся В объединении irqctx,
ассоциированном с локальным процессором. В этом случае макрос возвратит
ненулевое значение, потому что поле всегда положительно.
Последней структурой, важной для реализации softirq-функций, является
процессорная маска, описывающая висящие softirq-функций. Она хранится в
ПОЛе softirqpending Структуры irqcpustatt (ВСПОМНИМ, ЧТО у кажДОГО
процессора в системе есть своя такая переменная; см. табл. 4.8). Чтобы про-
прочитать или установить значение битовой маски, ядро пользуется макросом
locaisoftirqpendingo, который выбирает битовую маску softirq-функций
локального процессора.
Работа с softirq-функциями
Функция opensoftirqo отвечает за инициализацию softirq-функций. Она
принимает три параметра: индекс softirq-функций, указатель на softirq-
функцию, которая должна быть выполнена, и второй указатель на структуру
данных, которая может понадобиться softirq-функций. Функция ореп_
softirqo ограничивается инициализацией соответствующего элемента мас-
массива softirq_vec.
Softirq-функций активизируются функцией raisesoftirqo. Она принимает в
качестве параметра индекс softirq-функций пг и выполняет следующие дейст-
действия:
1. Выполняет макрос locaiirqsave, чтобы сохранить состояние флага if
регистра ef lags и отключить прерывания на локальном процессоре.
2. Помечает softirq-функцию как висящую, для чего устанавливает бит, соот-
соответствующий индексу пг, в битовой маске softirq-функций локального
процессора.
3. Если функция ininterrupt () возвращает 1, описываемая функция пере-
переходит к шагу 5. Эта ситуация означает, что либо функция raisesof tirq ()
была вызвана в контексте прерывания, либо softirq-функций в настоящий
момент запрещены.
4. В противном случае описываемая функция вызывает функцию
wakeupsoftirqdo, чтобы в случае необходимости разбудить поток ядра
ksoftirqd локального процессора.
5. Выполняет макрос locaiirqrestore, чтобы восстановить состояние фла-
флага if, сохраненное на шаге 1.
Проверки наличия активных (висящих) softirq-функций должны проводиться
регулярно, но так, чтобы не слишком увеличивать накладные расходы. Это
делается в нескольких точках кода ядра. Приведем список наиболее важных
таких точек (мы должны оговориться, что количество и позиции точек про-
проверки softirq-функций меняются от версии к версии ядра и в зависимости от
архитектуры):
□ когда ядро вызывает функцию locaibhenabieo15, чтобы разрешить
softirq-функций на локальном процессоре;
□ когда функция doiRQ () заканчивает обработку прерывания ввода/вывода
И ВЫЗЫВаеТ фуНКЦИЮ irq_exit ();
□ ПрИ НаЛИЧИИ КОНТрОЛЛера I/O APIC КОГДа фуНКЦИЯ smp_apic_timer_
interrupt о заканчивает обработку прерывания по локальному таймеру
(см. разд. "Архитектура хронометрирования в многопроцессорных сис-
системах" главы 6);
О в многопроцессорных системах — когда процессор заканчивает обработ-
обработку функции, вызванной межпроцессорным прерыванием call_function_
vector;
□ когда разбужен один из специальных потоков ядра ksoftirqd/n.
Функция dosoftirqO
Если на одной из перечисленных контрольных точек распознаны висящие
softirq-функций (значение макроса locaisoftirqpendingo не равно 0), ядро
вызывает функцию dosoftirqo для работы с ними. Эта функция выполняет
следующие действия:
1. Если функция ininterrupt о возвращает 1, описываемая функция завер-
завершает работу. Эта ситуация означает, либо что функция dosoftirqo была
вызвана в контексте прерывания, либо что softirq-функций в настоящий
момент запрещены.
2. Выполняет макрос locaiirqsave, чтобы сохранить состояние флага if и
отключить прерывания на локальном процессоре.
3. Если размер структуры threadunion равен 4 Кбайт, функция переключа-
переключается на стек мягких IRQ-запросов в случае необходимости. Этот шаг очень
15 Имя функции local_bh_enable () напоминает о специальном типе функций отложенного вы-
выполнения, который называется "функции нижней половины" (bottom-half), но в Linux 2.6 уже отсут-
отсутствует.
похож на шаг 2 функции doiRQ (), описанной ранее в этой главе. Конечно,
ВМеСТО массива hardirq_ctx ИСПОЛЬЗуетСЯ маССИВ sof tirq_ctx.
4. Вызывает функцию dosoftirqo (см. следующий раздел).
5. Если на шаге 3 фактически произошло переключение на стек гибких IRQ-
запросов, функция восстанавливает первоначальный указатель стека в ре-
регистре esp, тем самым переключаясь на стек исключений, который был в
работе до этого.
6. Выполняет макрос locaiirqrestore, чтобы восстановить состояние if
(локальные прерывания включены или отключены), сохраненное на ша-
шаге 2, а затем возвращает управление.
Функция dosoftirqO
Функция dosoftirqO читает битовую маску softirq-функций локального
процессора и выполняет функции отложенного выполнения, которые соот-
соответствуют установленным битам. При выполнении softirq-функций могут
"всплыть" новые висящие softirq-функций. Чтобы обеспечить малое время
задержки таких функций, функция dosoftirqO продолжает работать, пока
все висящие softirq-функций не будут выполнены. Этот подход, однако, мо-
может привести к тому, что функция do_sof tirq () будет работать достаточно
долго, задерживая процессы режима пользователя. По этой причине функция
dosoftirqO выполняет фиксированное количество итераций, а затем воз-
возвращает управление. Оставшиеся висящие softirq-функций, если таковые
имеются, будут в свое время обработаны потоком ядра ksoftirqd, описанным в
следующем разделе. Приведем краткое описание действий, выполняемых
функцией:
1. Инициализирует счетчик итераций значением 10.
2. Копирует битовую маску softirq-функций локального процессора (выбран-
(выбранную макросом localsof tirqpending ()) В локальную переменную pending.
3. Вызывает функцию locaibhdisableo, чтобы увеличить счетчик softirq-
функций. На первый взгляд кажется странным, что функции отложенно-
отложенного выполнения, должны быть запрещены перед началом их выполнения,
но в этом есть глубокий смысл. Поскольку функции отложенного выпол-
выполнения выполняются, в основном, при включенных прерываниях, по ходу
работы функции dosoftirqO может возникнуть прерывание. Когда
функция doiRQO выполняет функцию irqexito, может стартовать еще
один экземпляр функции dosoftirqO. Этого необходимо избежать,
поскольку функции отложенного выполнения должны выполняться на
процессоре последовательно. Поэтому первый экземпляр функции
dosof tirq () запрещает выполнение функций отложенного выполнения,
чтобы любой новый экземпляр этой функции закончил работу после ша-
шага 1 функции dosoftirqO .
4. Сбрасывает битовую карту softirq-функций локального процессора, что-
чтобы новые softirq-функций могли быть активизированы (битовая маска
уже сохранена в локальной переменной pending на шаге 2).
5. Вызывает функцию iocai_irq_enabie(), чтобы включить локальные пре-
прерывания.
6. Для каждого установленного бита в локальной переменной pending вызы-
вызывает соответствующую softirq-функцию. Вспомним, что адрес softirq-
функций с индексом п хранится в переменной sof tirqvec [n] ->action.
7. Вызывает фуНКЦИЮ local_irq_disable(), ЧТОбы ОТКЛЮЧИТЬ ЛОКаЛЬНЬЮ
прерывания.
8. Копирует битовую маску softirq-функций локального процессора в ло-
локальную переменную pending и еще раз уменьшает счетчик итераций.
9. Если переменная pending не равна 0 (то есть хотя бы одна softirq-функция
была активизирована после начала последней итерации) и счетчик итера-
итераций все еще положителен, функция возвращается к шагу 4.
10. Если имеются висящие softirq-функций, функция вызывает функцию
wakeupsoftirqdo, чтобы разбудить поток ядра, который отвечает за вы-
выполнение softirq-функций на локальном процессоре (см. следующий
раздел).
11. Вычитает 1 из счетчика softirq-функций, тем самым разрешая выполнение
функций отложенного выполнения.
Потоки ядра ksoftirqd
В последних версиях ядра у каждого процессора имеется собственный поток
ядра ksoftirqd/n (где п — логический номер процессора). Каждый поток ядра
ksoftirqd/n выполняет функцию ksoftirqd о, которая по сути представляет
собой следующий цикл:
for(;;) {
set_current_state(TASK_INTERRUPTIBLE );
schedule();
/* сейчас в состоянии TASK_RUNNING */
while (local_softirq_pending()) {
preempt_disable();
do_softirq();
preempt_enable();
cond_resched();
}
}
Будучи разбуженным, поток ядра проверяет битовую маску softirq-функций с
помощью макроса locaisoftirqpendingo и, если необходимо, вызывает
функцию dosoftirqo. Если висящих softirq-функций нет, эта функция пере-
переводит текущий процесс в состояние taskinterruptible и вызывает функцию
condreschedo, которая произведет переключение процессов, если этого
потребует текущий процесс (флаг tifneedresched текущего процесса уста-
установлен).
Поток ядра ksoftirqd/n представляет собой решение весьма важной проблемы,
требующей компромисса.
Softirq-функций способны повторно активизировать сами себя. Это могут
делать как сетевые softirq-функций, так и тасклеты. Кроме того, внешние со-
события, такие как лавина пакетов на сетевой карте, могут активизировать
softirq-функций с высокой частотой.
Возможность непрерывного выполнения большого количества softirq-
функций создает проблему, которая решается с помощью потоков ядра. Без
них разработчики оказываются лицом к лицу с двумя взаимоисключающими
стратегиями.
Первая заключается в игнорировании новых softirq-функций, появляющихся
во время работы функции dosoftirqo. Иными словами, при таком подходе
функция dosoftirqo определяла бы, какие softirq-функций висят к моменту
ее старта, и выполняла их. Затем она завершала бы работу без повторной
проверки висящих функций. Это решение неудачно. Предположим, некая
softirq-функция повторно активизируется во время выполнения функции
dosoftirqo. В худшем случае, эта softirq-функция не будет выполнена до
следующего прерывания по таймеру, даже если машина простаивает. Такое
время задержки неприемлемо, с точки зрения сетевых разработчиков.
Вторая стратегия заключается в постоянных проверках наличия висящих
softirq-функций. При этом подходе функция dosoftirqo все время проверя-
проверяла бы висящие softirq-функций и завершала бы работу, только когда их нет.
В то время как подобное решение, возможно, устроило бы сетевых
разработчиков, оно определенно вызовет недовольство среди обычных
пользователей. Если сетевая карта будет получать большой объем пакетов,
или softirq-функция будет непрерывно активизировать сама себя, то функция
dosoftirqo не возвратит управление, и работа программы в режиме пользо-
пользователя будет фактически остановлена.
Потоки ядра ksoftirqd/n пытаются найти компромисс в решении этой трудной
проблемы. Функция dosoftirqo определяет, какие softirq-функций висят, и
выполняет их. После нескольких итераций, если новые softirq-функций не
перестают появляться, функция будит поток ядра и завершает работу (шаг 10
в описании функции dosoftirqo). Поток ядра имеет низкий приоритет, и
у пользовательских программ появляется шанс выполниться. Если же маши-
машина простаивает, висящие softirq-функции будут быстро выполнены.
Тасклеты
Тасклеты являются предпочтительным инструментом реализации функций
отложенного выполнения в драйверах устройств ввода/вывода. Как было ска-
сказано ранее, тасклеты строятся поверх двух softirq-функций, hisoftirq и
taskletsoftirq. С одной softirq-функцией может быть ассоциировано не-
несколько тасклетов, каждый из которых несет собственную функцию. Между
названными двумя softirq-функциями нет никакой разницы, если не считать,
что функция dosoftirqo выполняет тасклеты функции hisoftirq раньше
тасклетов функции tasklet_softirq.
Обычные и высокоприоритетные тасклеты хранятся в массивах taskietvec и
taskiethivec соответственно. Каждый из них содержит nrcpus элементов
типа taskiethead, а каждый элемент содержит указатель на список
дескрипторов тасклетов. Дескриптор тасклета— это структура типа
taskietstruct, поля которой приведены в табл. 4.11.
Таблица 4.11. Поля дескриптора тасклета
Имя поля Описание
next Указатель на следующий дескриптор в списке
state Статус тасклета
count Счетчик блокировок
func Указатель на функцию тасклета
data Длинное целое без знака, которое может быть использовано функцией
тасклета
Поле state дескриптора тасклета включает в себя два флага:
О taskletstatesched— когда этот флаг установлен, он показывает, что
дескриптор тасклета занесен в один из списков массивов taskietvec или
tasklet_hi_vec.
□ taskletstaterun — когда этот флаг установлен, он показывает, что таск-
лет в настоящий момент выполняется. В однопроцессорных системах этот
флаг не используется, потому что там нет необходимости проверять, рабо-
работает ли данный тасклет.
Предположим, вы пишете драйвер устройства, и хотите использовать тасклет.
Что нужно сделать? Во-первых, вы должны выделить новую структуру
taskietstruct и инициализировать ее, вызвав функцию taskietinit (). Эта
функция принимает в качестве параметров адрес дескриптора тасклета, адрес
написанной вами функции тасклета и ее необязательный целочисленный ар-
аргумент.
Тасклет может быть избирательно отключен с помощью функции
tasklet_disable_nosync() ИЛИ tasklet_disable (). Обе ЭТИ функции увеличи-
вают поле count дескриптора тасклета, но последняя не возвращает управле-
управление, пока не завершится работающий экземпляр функции тасклета. Чтобы
ВКЛЮЧИТЬ ТаСКЛеТ, ПОЛЬЗуЙТеСЬ функцией tasklet_enable () .
Для активизации тасклета вы должны вызвать либо функцию taskiet_
schedule (), либо tasklethischedule (), В зависимости ОТ приоритета, КОТО-
рый вы придаете тасклету. Эти две функции очень похожи, и каждая выпол-
выполняет следующие действия:
1. Проверяет флаг taskletstatesched. Если он установлен, возвращает
управление (тасклет уже был запланирован к выполнению).
2. Вызывает функцию locaiirqsave, чтобы сохранить состояние флага if и
отключить локальные прерывания.
3. Заносит дескриптор тасклета в начало списка, на который указывает эле-
элемент tasklet_vec[n] ИЛИ tasklet_hi_vec [п], где п— логический НОМер ЛО-
кального процессора.
4. Вызывает функцию raisesoftirqirqoff о, чтобы активизировать softirq-
функцию taskletsoftirq или hisoftirq (эта вызванная функция анало-
аналогична функции raisesoftirqo, но предполагает, что локальные прерыва-
прерывания уже отключены).
5. Вызывает функцию locaiirqrestore, чтобы восстановить состояние
флага if.
В заключение раздела рассмотрим, как выполняется тасклет. Из предыдущего
раздела мы знаем, что активизированные softirq-функции выполняются
функцией dosoftirqO. Softirq-функция, ассоциированная с hisoftirq, на-
называется tasklet_hi_action(), а фуНКЦИЯ, ассоциированная С TASKLETSOFTIRQ,
называется taskietactiono. И снова две функции очень похожи друг на
друга. Каждая из них выполняет следующие действия:
1. Отключает локальные прерывания.
2. Получает логический номер п локального процессора.
3. Сохраняет адрес списка, на который указывает элемент taskiet_vec[n]
ИЛИ tasklethivec [n], В ЛОКальНОЙ переменной list.
4. Записывает null в качестве адреса в элемент taskiet_vec[n] или
taskiet_hi_vec[n], тем самым очищая список дескрипторов тасклетов, вы-
выбранных планировщиком.
5. Включает локальные прерывания.
6. Для каждого дескриптора тасклета в списке, на который указывает пере-
переменная list:
• в многопроцессорной системе проверяет флаг taskletstaterun таск-
тасклета:
D если он установлен, значит, тасклет того же типа уже выполняется
на другом процессоре. Тогда функция вставляет дескриптор таскле-
тасклета обратно в список, на который указывается элемент taskietvec [n]
или taskiet_hi_vec[n], и снова активизирует softirq-функцию
taskletsoftirq или hisoftirq. Таким образом, выполнение таскле-
тасклета откладывается до того момента, когда ни один тасклет того же
типа не будет выполняться на других процессорах;
D в противном случае на другом процессоре не работает тасклет дан-
данного типа, и функция устанавливает этот флаг, чтобы на других
процессорах нельзя было выполнить функцию тасклета;
• проверяет, не отключен ли тасклет, для чего анализирует поле count
дескриптора тасклета. Если тасклет отключен, функция сбрасывает
флаг taskletstaterun и возвращает дескриптор тасклета в список, на
КОТОрыЙ указывает Элемент tasklet_vec[n] ИЛИ tasklet_hi_vec[n]. За-
тем она снова активизирует softirq-функцию taskletsoftirq или
hi_softirq;
• если тасклет не отключен, функция сбрасывает флаг tasklet_
statesched и выполняет функцию тасклета.
Обратите внимание, что, если функция тасклета не активизирует сама себя,
каждая активизация тасклета приводит к выполнению, самое большее, одной
функции тасклета.
Рабочие очереди
Рабочие очереди впервые появились в Linux 2.6, заменив собой аналогичную
конструкцию "очередь задач" из Linux 2.4. Они делают возможной активиза-
активизацию функций ядра (во многом аналогичную активизации функций отложен-
отложенного выполнения) и последующее их выполнение специальными потоками
ядра, которые называются рабочими потоками:
Несмотря на определенное сходство, функции отложенного выполнения и
рабочие очереди сильно отличаются друг от друга. Основное различие за-
заключается в том, что функции отложенного выполнения работают в контек-
контексте прерывания, а рабочие очереди — в контексте процесса. Выполнение в
контексте процесса является единственным способом выполнить функции,
которые могут быть блокированы (например, функции, пытающиеся об-
обратиться к блоку данных на диске), потому что, как было замечено в
разд. "Вложенное выполнение обработчиков исключений и прерываний" ра-
ранее в этой главе, переключение процессов невозможно в контексте прерыва-
прерывания. Ни функции отложенного выполнения, ни функции в рабочей очереди не
могут обратиться к адресному пространству процесса в пользовательском
режиме. В самом деле, функция отложенного выполнения не может исходить
из каких-то предположений относительно процесса, работающего, когда она
выполняется. Что касается функции в рабочей очереди, она выполняется по-
потоком ядра, и у нее нет адресного пространства режима пользователя, к кото-
которому она могла бы обратиться.
Структуры данных для рабочих очередей
Основная структура, ассоциированная с рабочей очередью, — это дескриптор
по имени workqueuestruct, который среди прочих данных содержит массив
из nrcpus элементов (это максимальное количество процессоров в систе-
системеI6. Каждый элемент является дескриптором типа cpuworkqueuestruct,
поля которого перечислены в табл. 4.12.
Таблица 4.12. Поля структуры cpu_workqueue_struct
Имя поля Описание
lock Спин-блокировка для защиты структуры
remove_sequence Порядковый номер, используемый функцией f lush_workqueue ()
insert_sequence Порядковый номер, используемый функцией f lush_workqueue ()
worklist Голова списка висящих функций
morework Очередь ожидания, в которой спит рабочий поток в ожидании ра-
работы
workdone Очередь ожидания, в которой спят процессы, ожидающие очистки
рабочей очереди
wq Указатель на структуру workqueuestruct, содержащую этот де-
дескриптор
thread Указатель на дескриптор процесса рабочего потока структуры
16 Причина дублирования структур для рабочих очередей в многопроцессорных системах заключа-
заключается в том, что локальные для процессора структуры данных позволяют писать гораздо более эф-
эффективный код.
Таблица 4.12 (окончание)
Имя поля Описание
run_depth Текущая глубина выполнения функции run_workqueue () (это поле
может стать больше 1, если функция в списке рабочей очереди
будет заблокирована)
Поле worklist Структуры cpuworkqueuestruct ЯВЛЯетСЯ ГОЛОВОЙ ДВунапраВ-
ленного списка, в котором собраны висящие функции из рабочей очереди.
Каждая висящая функция представлена структурой workstruct, поля которой
перечислены в табл. 4.13.
Таблица 4.13. Поля структуры work_struct
Имя поля Описание
pending Установлено в 1, если функция уже стоит в рабочей очереди, в противном
случае равно О
entry Указатели на следующий и предыдущий элементы в списке висящих функ-
функций
func Адрес висящей функции
data Указатель, передаваемый в качестве параметра висящей функции
wq_data Обычно указывает на родительский дескриптор cpu_workqueue_struct
timer Программный таймер, применяемый для отсрочки выполнения висящей
функции
Функции рабочей очереди
Функция create_workqueue("foo") принимает в качестве параметра строку
символов и возвращает адрес дескриптора workqueuestruct для только что
созданной рабочей очереди. Кроме того, функция создает п рабочих потоков
(где п — количество процессоров, фактически присутствующих в системе),
названных в соответствии со строкой, переданной в качестве параметра:
fbo/O, foo/1 И Т. Д. Функция createsinglethreadworkqueue () работает анало-
гично, но создает только один рабочий поток, независимо от количества про-
процессоров. Чтобы разрушить рабочий поток, ядро вызывает функцию
destroyworkqueue (), которая принимает в качестве параметра указатель на
МаССИВ workqueue_struct.
Функция queuework () ставит в рабочую очередь функцию (уже упакованную
внутрь дескриптора workstruct). В качестве параметров она принимает ука-
затель wq на дескриптор workqueuestruct и указатель work на дескриптор
workstruct. Функция queueworko выполняет следующие действия:
1. Проверяет, не находится ли уже в очереди функция, подлежащая поста-
постановке в очередь (поле work->pending равно 1). Если находится, происходит
возврат управления.
2. Добавляет дескриптор workstruct в список рабочей очереди и устанавли-
устанавливает переменную work->pending в единицу.
3. Если рабочий поток спит в очереди morework дескриптора сри_
workqueuestruct, принадлежащего локальному процессору, функция бу-
будит его.
ФуНКЦИЯ queuedelayedwork () практически Идентична фуНКЦИИ queuework (),
но она принимает третий параметр, задающий задержку по времени в сис-
системных тактах (см. главу 6). Она используется для обеспечения минимальной
задержки перед выполнением висящей функции. На практике функция
queuedelayedwork () полагается в своей работе на программный таймер в
поле timer дескриптора workstruct, когда нужно отложить фактическую по-
постановку дескриптора workstruct в рабочую очередь. Функция cancei_
deiayedworko отменяет функцию из рабочей очереди, ранее запланирован-
запланированную к выполнению, при условии, что соответствующий дескриптор
workstruct еще не был занесен в список рабочей очереди.
Каждый рабочий поток непрерывно выполняет цикл внутри функции
workerthread (), причем большую часть времени поток спит в ожидании ра-
работы. Будучи разбуженным, он вызывает функцию runworkqueue (), которая
удаляет каждый дескриптор workstruct из списка рабочей очереди данного
потока и выполняет соответствующую висящую функцию. Поскольку функ-
функции из рабочей очереди могут блокироваться, рабочий поток может вернуть-
вернуться в состояние сна и даже мигрировать на другой процессор после пробужде-
пробуждения17.
Иногда ядру приходится ждать, пока выполнятся все висящие функции ра-
рабочей очереди. Функция flushworkqueue() принимает адрес дескриптора
workqueuestruct и блокирует вызвавший процесс, пока не закончится вы-
выполнение всех висящих функций из рабочей очереди. Однако функция
flushworkqueue() не ждет завершения функций, добавленных в рабочую
очередь после ее вызова, причем для распознавания только что добавленных
17 Странным образом, рабочий поток может быть выполнен на любом процессоре, а не только на
процессоре, соответствующем дескриптору cpu_workqueue_struct, которому принадлежит рабо-
рабочий поток. То есть, функция queue_work () заносит некую функцию в очередь локального процес-
процессора, но та функция может быть выполнена любым процессором в системе.
ВИСЯЩИХ функций ИСПОЛЬЗуЮТСЯ ПОЛЯ removesequence И inse resequence де-
СКрИПТОра cpu_workqueue_struct.
Заранее определенная рабочая очередь
В большинстве случаев создание целого набора рабочих потоков для выпол-
выполнения функции является лишней тратой ресурсов. Поэтому ядро предлагает
заранее определенную рабочую очередь, называемую events, которой может
свободно пользоваться любой разработчик. Эта очередь является всего лишь
стандартной рабочей очередью, которая может включать в себя функции из
разных слоев ядра и драйверов устройств ввода/вывода. Ее дескриптор
workqueuestruct хранится в массиве keventdwq. Для работы с такой заранее
определенной очередью ядро предлагает функции, перечисленные в
табл. 4.14.
Таблица 4.14. Вспомогательные функции для работы с заранее определенной
рабочей очередью
Функция заранее определенной Эквивалентная функция стандартной
рабочей очереди рабочей очереди
schedule_work(w) queue_work(keventd_wq,w)
schedule_delayed_work(w,d) queue_delayed_work(keventd_wq,w,d)
(на любом процессоре)
schedule delayed work on(cpu,w,d) queue delayed work(keventd wq,w,d)
(на данном процессоре)
flush_scheduled_work() flush_workqueue(keventd_wq)
Заранее определенная рабочая очередь позволяет значительно сэкономить
системные ресурсы, если функция вызывается редко. С другой стороны,
функции, выполняемые в этой очереди, не должны блокироваться надолго.
Поскольку выполнение висящей функции из рабочей очереди сериализуется
на каждом процессоре, большая задержка отрицательно сказывается на ос-
остальных пользователях заранее определенной рабочей очереди. В дополнение
к общей очереди events в Linux 2.6 можно найти несколько специализирован-
специализированных рабочих очередей. Самой важной из них является kblockd, используемая
слоем блочных устройств (см. главу 14).
Возврат из прерываний и исключений
Мы закончим эту главу рассмотрением заключительного этапа выполнения
обработчиков исключений и прерываний. (Возврат из системного вызова яв-
является специальным случаем, и он обсуждается в главе 10). Хотя основная
цель этого этапа — возобновить выполнение некоторой программы, при его
реализации необходимо учитывать несколько моментов:
□ количество управляющих трактов ядра, выполняемых параллельно (если
поток всего один, процессор должен переключиться обратно в режим
пользователя);
П висящие запросы на переключение процесса (если имеется такой запрос,
ядро должно выполнить планирование; в противном случае управление
возвращается текущему процессу);
□ сигналы, ожидающие доставки (если текущему процессу послан сигнал,
он должен быть обработан);
□ пошаговый режим (если выполнение текущего процесса отслеживается
отладчиком, пошаговый режим должен быть восстановлен до переключе-
переключения в пользовательский режим);
□ режим виртуального 8086 (если процессор работает в режиме виртуально-
виртуального процессора 8086, текущий процесс выполняет старую программу ре-
реального режима и, следовательно, должен быть обработан специальным
образом).
Для отслеживания висящих запросов на переключение процесса, сигналов,
ожидающих доставки, и пошагового режима применяется ряд флагов. Они
хранятся в поле flags дескриптора threadinf о. Это поле содержит и другие
флаги, но они не имеют отношения к возврату из прерываний и исключений.
Полный список этих флагов приведен в табл. 4.15.
Таблица 4.15. Поле flags дескриптора thread_info
Имя флага Описание
tif_syscall_trace Отслеживаются системные вызовы
tif_notify_resume На платформе 80x86 не используется
tif_sigpending Процесс имеет сигналы, ожидающие доставки
tif_need_resched Должно быть выполнено планирование
tif_singlestep Необходимо восстановить пошаговый режим при возвращении
в режим пользователя
tifiret Форсировать возврат из системного вызова через команду
iret, а не sysexit
tif_syscall_audit Отслеживаются системные вызовы
tif_polling_nrflag Простаивающий процесс опрашивает флаг tif_need_resched
tifmemdie Процесс в настоящий момент уничтожается с целью утилиза-
утилизации памяти (см. разд. "Уничтожение процессов из-за нехватки
памяти" в главе 17)
Ассемблерный код ядра, который выполняет все эти действия, строго говоря,
не является функцией, потому что не возвращает управление функциям,
которые его вызвали. Этот фрагмент кода имеет две точки входа:
ret_from_intr () и ret_from_exception (). Как следует из их названий, ядро
входит в первую точку, когда завершает обработчик прерывания, и во вто-
вторую — когда завершает обработчик исключения. Далее мы будем называть
эти точки входа функциями, чтобы упростить изложение.
Общая блок-схема, включающая в себя эти точки входа, изображена на
рис. 4.6. Серые блоки обозначают ассемблерные инструкции, реализующие
Рис. 4.6. Возврат из прерываний и исключений
вытеснение в ядре (см. главу 5). Если вы хотите разобраться, как работает яд-
ядро, откомпилированное без поддержки вытеснения, просто игнорируйте се-
серые блОКИ. На ЭТОЙ блОК-СХеме ТОЧКИ ВХОДа ret_from_exception() И
retf romintr () выглядят почти одинаково. Разница проявляется, только если
при компиляции ядра была выбрана опция поддержки вытеснения. В этом
случае локальные прерывания отключаются сразу после возврата из исклю-
исключений.
Эта блок-схема дает общее представление о действиях, которые необходимо
выполнить, чтобы возобновить выполнение прерванной программы. Перей-
Перейдем к деталям и обсудим ассемблерный код.
Точки входа
Точки входа ret_from_intr() и ret_from_exception () практически эквива-
лентны следующему фрагменту кода:
ret_f rom_exception:
cli ; отсутствует, если вытеснение в ядре не поддерживается
ret_f rom_intr:
movl $-8192, %ebp ; -4096, если используется несколько стеков режима
ядра
andl %esp, %ebp
movl 0x30(%esp), %eax
movb 0x2c(%esp), %al
testl $0x00020003, %eax
jnz resume_userspace
jpm resume_kernel
Вспомним, что при возврате из прерывания локальные прерывания отключе-
отключены (см. шагЗ в описании функции handieiRQevento). Поэтому ассемблер-
ная инструкция cli выполняется только при возврате из исключения.
Ядро загружает адрес дескриптора threadinf о текущего процесса в регистр
ebp (см. главу 3).
Затем значения регистров cs и ef lags, которые были помещены в стек, когда
произошло прерывание или исключение, используются для определения, ра-
работала ли прерванная программа в режиме пользователя, либо установлен ли
флаг vm в регистре efiags18. В любом случае совершается переход на метку
resumeuserspace. Если результат проверки отрицателен, переход делается на
метку resume_kernel.
18 Когда этот флаг установлен, программы выполняются в виртуальном режиме 8086; подробности
см. в документации по процессорам Pentium.
Возобновление управляющего тракта ядра
Ассемблерный код с меткой resumekernei выполняется, если возобновляемая
программа работает в режиме ядра:
resume_kernel:
cli ; эти три инструкции
cmpl $0, 0x14(%ebp) ; отсутствуют, если вытеснение в ядре
jz need_resched ; не поддерживается
restore_all:
popl %ebx
popl %ecx
popl %edx
popl %esi
popl %edi
popl %ebp
popl %eax
popl %ds
popl %es
addl $4, %esp
iret
Если поле preemptcount дескриптора threadinf о равно нулю (вытеснение в
ядре включено), ядро переходит по метке needresched. В противном случае
необходимо возобновить выполнение прерванной программы. Функция за-
загружает в регистры значения, сохраненные, когда возникло прерывание или
исключение, и получает управление, выполнив инструкцию iret.
Проверка вытеснения в ядре
Когда выполняется этот код, ни один из незавершенных управляющих трак-
трактов ядра не является обработчиком исключения. В противном случае поле
preemptcount было бы больше нуля. Однако, как было сказано в разд. "Вло-
"Вложенное выполнение обработчиков исключений и прерываний" ранее в этой
главе, возможно наличие до двух управляющих трактов ядра, ассоциирован-
ассоциированных с исключениями (помимо того, который завершается).
need_resched:
movl 0x8(%ebp), %ecx
testb $ A«TIF_NEED_RESCHED) , %cl
jz restore_all
testl $0x00000200,0x30(%esp)
jz restore_all
call preempt_schedule_irq
jrnp need_resched
ЕСЛИ флаг TIF_NEED_RESCHED В ПОЛе flags дескриптора current->thread_info
равен нулю, переключение процессов не требуется, и совершается переход на
метку restoreaii. Переход на ту же метку совершается, если возобновляе-
возобновляемый управляющий тракт ядра работал при отключенных локальных преры-
прерываниях. В этом случае переключение процессов могло бы повредить структу-
структуры данных ядра (подробности см. в разд. "Когда синхронизация необходима"
главы 5).
Если требуется переключение процесса, вызывается функция preempt_
scheduie_irq(). Она устанавливает флаг preempt_active в поле preempt_count,
временно записывает-1 в счетчик глобальных блокировок ядра (см.
разд. "Глобальная блокировка ядра" главы 5), включает локальные прерыва-
прерывания и вызывает функцию schedule (), чтобы выбрать другой процесс. Когда
ВОЗОбнОВИТСЯ Выполнение ПерВОГО Процесса, фуНКЦИЯ preempt_schedule_irq()
восстанавливает предыдущее значение счетчика глобальных блокировок,
сбрасывает флаг preemptactive и отключает локальные прерывания. Функ-
Функция schedule о будет многократно вызвана, пока флаг tifneedresched те-
текущего процесса остается установленным.
Возобновление программы режима пользователя
Если программа, выполнение которой должно быть возобновлено, работает
в режиме пользователя, совершается переход на метку resumeuserspace:
resume_userspace:
cli
movl 0x8(%ebp), %ecx
andl $0x0000ff6e, %ecx
je restore_all
jmp work_pending
После отключения локальных прерываний производится проверка значения
поля flags дескриптора current->thread_info. Если никакой флаг, кроме
TIF_SYSCALL_TRACE, TIF_SYSCALL_AUDIT И TIF_S INGLE STEP, He установлен, ЗНаЧИТ,
больше ничего предпринимать не нужно. Совершается переход на метку
restoreaii, что приводит к возобновлению работы программы пользова-
пользовательского режима.
Проверка необходимости перепланирования
Флаги в дескрипторе threadinfo говорят о том, что перед возобновлением
прерванной программы должна быть выполнена дополнительная работа.
work_pending:
testb $ A«TIF_NEED_RESCHED) , %cl
jz work notifysig
work_resched:
call schedule
cli
jmp resume_userspace
Если имеется висящий запрос на переключение процессов, вызывается функ-
функция schedule о, выбирающая, какой процесс должен быть выполнен. Когда
возобновится выполнение предыдущего процесса, совершается переход на
метку resume_userspace.
Обработка висящих сигналов,
режима virtual-8086 и пошагового режима
В этом случае, помимо переключения процессов, должна быть проделана до-
дополнительная работа:
work_notifysig:
movl %esp, %eax
testl $0x00020000, 0x30(%esp)
je If
work_noti fys ig_v8 6:
pushl %ecx
call save_v86_state
popl %ecx
movl %eax, %esp
1:
xorl %edx, %edx
call do_notify_resume
jmp restore_all
Если управляющий флаг vm в регистре ef lags программы в режиме пользова-
пользователя установлен, вызывается функция save_v86_state(), которая строит
структуры режима виртуального 8086 в адресном пространстве режима поль-
пользователя. Затем вызывается функция do_notify_resume (), которая позаботится
о сигналах, ожидающих доставки в пошаговом режиме. Затем, чтобы
возобновить прерванную программу, совершается переход на метку
restore_all.
ГЛАВА 5
Синхронизация в ядре
Можно считать ядро неким сервером, отвечающим на запросы. Эти запросы
поступают либо от процесса, выполняемого процессором, либо от внешнего
устройства, генерирующего запрос на прерывание. Мы проводим эту анало-
аналогию, чтобы подчеркнуть, что фрагменты кода ядра выполняются не последо-
последовательно, а вперемешку. В результате между ними может возникнуть конку-
конкуренция за ресурсы, и эту ситуацию следует контролировать с помощью соот-
соответствующих приемов синхронизации. Введение в эту тему изложено в
главе 1.
Мы начнем эту главу с разъяснений, когда и до какой степени запросы к ядру
выполняются перемежающимся способом. Затем мы представим читателю
базовые примитивы синхронизации, реализуемые ядром, и опишем их при-
применение в наиболее типичных случаях. В конце главы мы приведем несколь-
несколько практических примеров.
Как ядро обслуживает запросы
Чтобы лучше понять, как выполняется код ядра, представим себе ядро в виде
официанта, который должен удовлетворять запросы двух типов: поступаю-
поступающие от клиентов и исходящие от некоторого ограниченного количества ме-
менеджеров. Официант придерживается следующих правил:
1. Если менеджер зовет официанта, когда тот свободен, официант выполняет
его указания.
2. Если менеджер зовет официанта, когда тот обслуживает клиента, офици-
официант оставляет клиента и выполняет указания менеджера.
3. Если менеджер зовет официанта, когда тот выполняет указания другого
менеджера, официант приостанавливает выполнение указаний первого
менеджера и приступает к выполнению указаний второго. Когда он вы-
выполнит указания второго менеджера, он вернется к указаниям первого.
4. Один из менеджеров может приказать официанту оставить клиента, об-
обслуживаемого в настоящий момент.
5. Выполнив указания всех менеджеров, официант может временно приоста-
приостановить обслуживание своего клиента и перейти к новому.
Действия, выполняемые официантом, соответствуют коду, выполняемому
процессором в режиме ядра. Когда процессор работает в режиме пользовате-
пользователя, считается, что официант свободен.
Указания менеджеров соответствуют прерываниям, а запросы клиентов —
системным вызовам или исключениям, возбуждаемым процессами в режиме
пользователя. В главе 10 подробно описано, что процесс режима пользовате-
пользователя, обращающийся к ядру с запросом, должен выдать соответствующую ин-
инструкцию (в архитектуре 80x86 это инструкция int $0x80 или sysenter). Та-
Такие инструкции возбуждают исключение, заставляющее процессор переклю-
переключиться из режима пользователя в режим ядра. Далее в этой главе мы общим
термином "исключение" обозначаем как системные вызовы, так и обычные
исключения.
Внимательный читатель уже связал первые три правила официанта с вложен-
вложенностью управляющих трактов ядра, описанной в главе 4. Четвертое правило
соответствует одной из интереснейших функциональных возможностей, по-
появившейся в ядре Linux 2.6, а именно — вытеснению в ядре.
Вытеснение в ядре
Удивительно, но дать хорошее определение для вытеснения в ядре очень
трудно. С первой попытки можно сказать, что ядро реализует вытеснение,
если переключение процессов может произойти, когда замещаемый процесс
выполняет функцию ядра, т. е. работает в режиме ядра. К сожалению, в Linux
(как и любой другой реальной операционной системе) все обстоит гораздо
сложнее:
□ как в вытесняющих, так и в невытесняющих ядрах процесс, работающий в
режиме ядра, может добровольно освободить процессор, например, пото-
потому что ему приходится остановиться в ожидании некоторого ресурса. Та-
Такую ситуацию мы назовем запланированным переключением процессов.
Однако вытесняющее ядро отличается от невытесняющего тем, как про-
процесс, работающий в режиме ядра, реагирует на асинхронные события,
которые могут привести к переключению процессов (пример такого собы-
события — обработчик прерываний будит процесс с более высоким приорите-
приоритетом). Подобный вид переключения процессов мы назовем форсиро-
форсированным',
□ все переключения процессов выполняются макросом switchto. Как в вы-
вытесняющих, так и в невытесняющих ядрах переключение процессов про-
происходит, когда процесс закончил выполнять некоторый поток ядра, и вы-
вызван планировщик. Однако в невытесняющих ядрах текущий процесс не
может быть замещен, если он не собирается в данный момент переклю-
переключиться в режим пользователя.
Таким образом, главной характеристикой вытесняющего ядра является тот
факт, что процесс, работающий в режиме ядра, может быть замещен другим
процессом прямо в середине выполнения функции ядра.
Приведем пару примеров для иллюстрации разницы между вытесняющим и
невытесняющим ядром.
Когда процесс А выполняет обработчик исключения (обязательно в режиме
ядра), становится выполняемым процесс В с более высоким приоритетом.
Это может произойти, например, если возникнет прерывание, и соответст-
соответствующий обработчик разбудит процесс В. Если ядро реализует вытеснение,
форсированное переключение процессов приводит к замене процесса А про-
процессом В. Обработчик исключения остается незавершенным, и его выполне-
выполнение продолжится только после того, как планировщик снова выберет про-
процесс А. Если же ядро является невытесняющим, переключения процессов не
произойдет, пока процесс А либо не закончит выполнение обработчика ис-
исключения, либо не освободит процессор добровольно.
В качестве еще одного примера рассмотрим процесс, у которого истекает
квант времени (см. разд. "Функция schedule_tick()" главы 7), пока он выпол-
выполняет обработчик исключения. Если ядро является вытесняющим, процесс
может быть замещен немедленно, но в невытесняющем ядре процесс будет
продолжаться, пока он либо не закончит выполнение обработчика исключе-
исключения, либо не освободит процессор добровольно.
Основной целью реализации вытеснения в ядро является сокращение пере-
переходного состояния процессов режима пользователя, т. е. времени между мо-
моментом, когда процесс стал выполняемым, и моментом, когда он фактически
стал выполняться. Процессы, выполняющие задачи, требующие точного пла-
планирования времени (контроллеры внешней аппаратуры, датчики состояния
окружающей среды, видеоплееры и т. д.), реально выигрывают от вытеснения
в ядре, поскольку уменьшается риск, что они будут задержаны другим про-
процессом, работающим в режиме ядра.
Превращение ядра Linux 2.6 в вытесняющее не потребовало резких измене-
изменений в схеме его работы по сравнению со старыми невытесняющими версия-
версиями. Как показано в главе 4, вытеснение в ядре отключено, если поле
preemptcount в дескрипторе threadinfo, на который ссылается макрос
currentthreadinf о (), больше нуля. В поле закодированы три различных
счетчика, как показано в табл. 4.10, поэтому значение его больше нуля в лю-
любом из следующих случаев:
□ ядро выполняет служебную процедуру обработки прерывания;
□ функции отложеннного выполнения запрещены (это всегда так, если ядро
выполняет softirq-функцию или тасклет);
□ вытеснение в ядре отключено явным образом путем установки счетчика
вытеснений в положительное значение.
Отсюда следует, что вытеснение в ядре возможно, только когда ядро выпол-
выполняет обработчик исключения (в частности, системный вызов), и вытеснение
не было отключено явно. Кроме того, как было показано в главе 4, у локаль-
локального процессора должны быть включены локальные прерывания, в против-
противном случае вытеснение в ядре выполняться не будет.
Несколько простых макросов, приведенных в табл. 5.1, работают со счетчи-
счетчиком вытеснений, хранящимся в поле premptcount.
Таблица 5.1. Макросы, работающие со счетчиком вытеснений
Макрос Описание
preempt_count () Выбирает поле preempt_count в дескрипторе
thread, info
preempt_disable() Увеличивает значение счетчика вытеснений на еди-
единицу
preempt_enable_no_resched () Уменьшает значение счетчика вытеснений на еди-
единицу
preempt_enable () Уменьшает значение счетчика вытеснений на еди-
единицу и вызывает функцию preempt_schedule (),
если флаг tif__need_resched в дескрипторе thread_
info установлен
get_cpu() Аналогичен макросу preempt_disable (), но допол-
дополнительно возвращает номер локального процессора
put_cpu () То же, что preempt_enable ()
put_cpu_no_resched () То же, что preempt_enable_no_resched ()
Макрос preemptenabie () уменьшает счетчик вытеснений, а затем проверяет,
установлен ли флаг tifneedresched (cm. табл. 4.15). В этом случае "висит"
запрос на переключение процессов, и поэтому макрос вызывает функцию
preemptscheduie (), которая фактически выполняет следующий код:
if (!current_thread_info->preempt_count && ! irqs_disabled() ) {
current_thread_info->preempt_count = PREEMPT_ACTIVE;
schedule();
current_thread_info->preempt_count = 0;
}
Функция проверяет, включены ли локальные прерывания, и равно ли нулю
поле preemptcount у текущего процесса. Если оба условия выполнены, она
вызывает функцию schedule о, чтобы выбрать другой процесс. Таким обра-
образом, вытеснение в ядре может произойти либо когда прекращается выполне-
выполнение управляющего тракта ядра (обычно это обработчик прерываний), либо
когда обработчик исключения заново включает вытеснение с помощью
макроса preemptenabie (). Как мы увидим далее в разд. 'Запрещение и раз-
разрешение функций отложенного выполнения" этой главы, вытеснение в ядре
может также иметь место, когда включаются функции отложенного выпол-
выполнения.
Мы завершим этот раздел замечанием о том, что вытеснение в ядре требует
расходов, с которыми нельзя не считаться. По этой причине в Linux 2.6 суще-
существует опция конфигурации ядра, позволяющая пользователям включать или
выключать вытеснение при компиляции ядра.
Когда синхронизация необходима
В главе 1 были введены такие понятия, как конфликт одновременного обра-
обращения (гонка) и критическая область для процессов. Те же определения спра-
справедливы и в отношении управляющих трактов ядра. В контексте этой главы
конфликт одновременного обращения может возникнуть, когда результат вы-
вычислений зависит от вложенности двух и более перемежающихся трактов яд-
ядра. Критическая область — это участок кода, который должен быть до конца
выполнен трактом ядра, вошедшим в него, прежде чем в него сможет войти
другой тракт ядра.
Чередование трактов ядра усложняет жизнь разработчикам; они должны быть
исключительно внимательны при идентификации критических областей в
обработчиках исключений, обработчиках прерываний, функциях отложенно-
отложенного выполнения и потоках ядра. Когда критическая область локализована, она
должна быть соответствующим образом защищена, чтоб в любой момент
времени внутри ее находилось не более одного управляющего тракта ядра.
Предположим, например, что двум разным обработчикам прерываний необ-
необходимо обратиться к одной структуре, содержащей несколько взаимосвязан-
взаимосвязанных переменных, скажем, буфер и целое, показывающее его длину. Все опе-
операторы, затрагивающие эту структуру, должны быть помещены в одну кри-
критическую область. Если в системе только один процессор, критическая
область может быть защищена отключением прерываний на время обраще-
ния к совместно используемой структуре, поскольку вложенность управляю-
управляющих трактов ядра может иметь место только при включенных прерываниях.
С другой стороны, если к структуре обращаются только служебные процеду-
процедуры системных вызовов, а в системе один процессор, то критическую область
можно защитить простым отключением вытеснения в ядре на время обраще-
обращения к совместно используемой структуре.
Читатель уже догадался, что в многопроцессорных системах ситуация гораз-
гораздо сложнее. Несколько процессоров могут одновременно выполнять код яд-
ядра, и разработчики не могут надеяться, что к структуре можно без опаски об-
обращаться только потому, что вытеснение в ядре отключено, а к структуре ни-
никогда не обращается обработчик прерываний или softirq-функция.
В следующих разделах мы увидим, что ядро предлагает разработчикам ши-
широкий набор разнообразных приемов синхронизации. Разработчикам остается
лишь решать каждую проблему синхронизации наиболее подходящими спо-
способами.
Когда в синхронизации нет необходимости
Некоторые подходы к проектированию ядра, обсуждавшиеся в предыдущей
главе, в определенной степени облегчают синхронизацию потоков ядра. При-
Приведем их краткий обзор:
□ все обработчики прерываний подтверждают получение прерывания на
программируемом контроллере прерываний, а также отключают линию
IRQ. To же самое прерывание больше не возникает, пока не завершится
обработчик;
П обработчики прерываний, softirq-функции и тасклеты не могут быть ни
вытеснены, ни блокированы, поэтому их выполнение не может быть при-
приостановлено на долгое время. В худшем случае возможна некоторая за-
задержка из-за других прерываний, возникших по ходу выполнения этих
функций (это и есть вложенное выполнение трактов ядра);
□ тракт ядра, обрабатывающий прерывание, не может быть прерван трактом,
выполняющим функцию отложенного выполнения или служебную про-
процедуру системного вызова;
□ softirq-функции и тасклеты не могут чередоваться на одном процессоре;
□ один тасклет не может выполняться одновременно на нескольких процес-
процессорах.
Каждое из этих решений можно рассматривать как своего рода ограничение,
которое можно эксплуатировать для облегчения кодирования некоторых
функций ядра.
Приведем несколько примеров возможных упрощений:
□ нет необходимости кодировать обработчики прерываний и тасклеты как
реентерабельные функции;
□ процессорные переменные, к которым обращаются только softirq-функции
и тасклеты, не требуют синхронизации;
□ структура, к которой обращается только один тасклет, не требует синхро-
синхронизации.
Оставшаяся часть этой главы посвящена тому, что надо делать, когда син-
синхронизация все-таки необходима, т. е. тому, как избежать порчи данных при
небезопасных обращениях к совместно используемым структурам.
Примитивы синхронизации
Сейчас мы обсудим, как можно чередовать потоки ядра, избегая при этом
конфликтов обращения к совместно используемым данным. В табл. 5.2 дан
список приемов синхронизации, используемых в ядре Linux. Столбец "Об-
"Область применения" показывает, применима ли данная техника ко всем про-
процессорам в системе или только к одному. Например, отмена локальных пре-
прерываний действительна только для одного процессора (остальные процессо-
процессоры она не затрагивает). И наоборот, атомарная операция действует на все
процессы в системе (атомарные операции на нескольких процессорах не мо-
могут чередоваться при обращении к одной структуре данных).
Таблица 5.2. Различные приемы синхронизации, используемые ядром
"Рием [описание [^Тнения
Процессорные Дублирование структуры данных Все процессоры
переменные для каждого процессора
Атомарная операция Атомарная операция Все процессоры
чтения/изменения/записи по отношению
к счетчику
Барьер памяти Недопущение изменения порядка Локальный процессор
инструкций или все процессоры
Спин-блокировка Блокировка без прекращения Все процессоры
выполнения
Семафор Блокирование процесса Все процессоры
(перевод в состояние сна)
Seqlock-блокировка Блокировка, основанная на счетчике Все процессоры
обращений
Таблица 5.2 (окончание)
_ _ Область
ПРием Описание применения
Отмена локальных Запрет на обработку прерываний Локальный процессор
прерываний на одном процессоре
Отмена локальных Запрет на выполнение функций Локальный процессор
softirq-функций отложенного выполнения на одном
процессоре
Обновление копии Доступ к совместно используемым Все процессоры
для чтения (RCU) структурам при помощи указателей,
без блокировок
Сейчас мы кратко обсудим каждый способ синхронизации. Затем далее в
разд. "Синхронизация обращений к структурам данных ядра" этой главы мы
покажем, как сочетать эти способы для эффективной защиты данных ядра.
Процессорные переменные
Наилучшая техника синхронизации состоит в разработке ядра, в первую оче-
очередь, таким образом, чтобы вообще избежать необходимости в синхрониза-
синхронизации. Как мы вскоре убедимся, любой специальный примитив синхронизации
дорого обходится в плане производительности системы.
Простейшим и самым эффективным приемом синхронизации является объ-
объявление переменных ядра как процессорных переменных. В принципе, про-
процессорная переменная — это массив структур, по одной на каждый процессор
системы.
Процессор не вправе обращаться к элементам массива, соответствующим
другим процессорам. С другой стороны, он может свободно читать и моди-
модифицировать собственный элемент, не опасаясь конфликтов одновременного
обращения, поскольку является единственным процессором, наделенным
этим правом. Отсюда следует, что процессорными переменными можно
пользоваться только в отдельных случаях, в основном, когда имеет смысл
распределить данные между процессорами в системе.
Элементы процессорного массива выровнены в основной памяти так, что
каждая структура данных попадает на отдельную строку аппаратного кэша
(см. главу 2). Поэтому одновременные обращения к процессорному массиву
не приводят к снупингу и недействительности строк кэша, довольно дорогих
операций, с точки зрения производительности системы.
Хотя процессорные переменные обеспечивают защиту от одновременного
обращения со стороны нескольких процессоров, они не защищают от одно-
временного обращения со стороны асинхронных функций (обработчиков
прерываний и функций отложенного выполнения). В таких случаях требуют-
требуются дополнительные примитивы синхронизации.
Кроме того, процессорные переменные уязвимы перед конфликтами одно-
одновременного обращения, вызванными вытеснением в ядре, как в однопроцес-
однопроцессорных, так и в многопроцессорных системах. В качестве общей рекоменда-
рекомендации, управляющий тракт ядра должен обращаться к процессорной перемен-
переменной при отключенном вытеснении в ядре. Представьте, например, что может
случиться, если тракт ядра получит адрес локальной копии процессорной пе-
переменной, а затем будет вытеснен и перенесен на другой процессор: у потока
останется адрес элемента, принадлежащего первому процессору.
В табл. 5.3 перечислены основные функции и макросы, предлагаемые ядром
для работы с процессорными переменными.
Таблица 5.3. Функции и макросы для работы с процессорными переменными
Имя функции или макроса Описание
DEFiNE_PER_CPU(type, name) Статически выделяет процессорный массив name
структур типа type
per_cpu(name, cpu) Выбирает элемент процессорного массива name для
процессора cpu
get_cpu_var (name) Выбирает элемент процессорного массива name для
локального процессора
get_cpu_var(name) Отключает вытеснение в ядре, а затем выбирает
элемент процессорного массива name для локального
процессора
put_cpu_var (name) Включает вытеснение в ядре (параметр name не ис-
используется)
alloc_percpu(type) Динамически выделяет процессорный массив струк-
структур типа type и возвращает его адрес
free_percpu (pointer) Освобождает динамически выделенный процессор-
процессорный массив, находящийся по адресу pointer
per_cpu_ptr (pointer, cpu) Возвращает адрес элемента процессорного массива,
находящегося по адресу pointer, для процессора cpu
Атомарные операции
Существует несколько ассемблерных инструкций вида "чтение — модифика-
модификация — запись". Они обращаются к ячейке памяти дважды: первый раз, чтобы
прочитать старое значение, а второй — чтобы записать новое.
Предположим, что два управляющих тракта ядра, работающие на двух про-
процессорах, пытаются одновременно "прочитать— модифицировать— запи-
записать" содержимое одной ячейки памяти, пользуясь неатомарными операция-
операциями. Вначале оба процессора попробуют прочитать содержимое ячейки, но
арбитр памяти (электронная схема, выстраивающая в цепочку обращения к
чипам памяти) вмешается, предоставит доступ к ячейке только одному про-
процессору и задержит второй. Когда первая операция чтения завершится, вто-
второй процессор прочитает точно такое же (старое) значение из ячейки. Затем
оба процессора попытаются записать в ячейку одно и то же (новое) значение.
Арбитр памяти опять сериализует доступ к ячейке, и, в конечном счете, обе
операции записи закончатся успешно. Тем не менее глобальный результат
некорректен, потому что два процессора записали одно и то же (новое) зна-
значение. Таким образом, две чередующиеся операции "чтения— модифика-
модификации — записи" сработали как одна.
Простейший способ предотвращения конфликтов одновременного обраще-
обращения, вызванных инструкциями "чтения — модификации — записи", состоит в
обеспечении атомарности этих операций на уровне чипа. Каждая такая опе-
операция должна выполняться как одна инструкция, не обрывающаяся на сере-
середине, что позволит избежать обращений к той же ячейке памяти со стороны
других процессоров. Эти элементарные атомарные операции лежат в основе
других, более гибких механизмов создания критических областей.
Рассмотрим инструкции 80x86 в соответствии с этой классификацией:
□ ассемблерные инструкции, которые выполняют одно обращение к выров-
выровненным данным или вообще не обращаются к памяти, являются атомар-
атомарными1;
□ ассемблерные инструкции "чтения — модификации — записи" (такие как
inc или dec), которые читают данные из памяти, обновляют их и записы-
записывают в память новое значение, атомарны, если никакой другой процессор
не обратился к шине памяти после чтения, но до записи. Подобная ситуа-
ситуация, в принципе, невозможна в однопроцессорной системе;
□ ассемблерные инструкции "чтения — модификации — записи", у которых
коду операции предшествует байт lock (oxf о), атомарны даже в многопро-
многопроцессорной системе. Когда управляющий блок распознает префикс, он "за-
"заблокирует" шину памяти до окончания инструкции. Таким образом, дру-
другой процессор не будет иметь доступа к ячейке, пока выполняется инст-
инструкция с блокировкой;
1 Элемент данных выровнен в памяти, если его адрес кратен его размеру в байтах. Например, адрес
выровненного короткого целого должен быть кратен двум, а адрес выровненного целого — четы-
четырем. Вообще говоря, обращение к невыровненным данным не является атомарным.
□ ассемблерные инструкции, у которых коду операции предшествует байт
rep (Oxf 2, Oxf з, который заставляет управляющий блок несколько раз по-
повторить инструкцию), не являются атомарными. Перед выполнением
новой операции управляющий блок проверяет наличие "висящих" преры-
прерываний.
Когда вы пишете программу на языке С, у вас нет гарантии, что компилятор
воспользуется атомарной инструкцией для такого оператора, как а=а+1 или
даже а++. Поэтому ядро Linux предоставляет разработчику специальный тип
atomict (счетчик, доступный атомарным образом) и набор специальных
функций и макросов (табл. 5.4), которые позволяют работать с переменными
типа atomict и реализованы в виде одиночных атомарных инструкций ас-
ассемблера. В многопроцессорных системах каждая такая инструкция в качест-
качестве префикса имеет байт lock.
Другой класс атомарных функций работает с битовыми масками (табл. 5.5).
Таблица 5.4. Атомарные операции в Linux
Функция Описание
atomic_read(v) Возвратить *v
atomic_set (v, i) Записать в *v значение i
atomic_add(i,v) Прибавить i к *v
atomic_sub(i,v) Вычесть i из *v
atomic_sub_and_test (i, v) Вычесть i из *v и возвратить 1, если результат
равен нулю; в противном случае возвратить О
atomic_inc(v) Прибавить 1 к *v
atomic_dec (v) Вычесть 1 из *v
atomic_dec_and_test (v) Вычесть 1 из *v и возвратить 1, если результат
равен нулю; в противном случае возвратить О
atomic_inc_and_test (v) Прибавить 1 к *v и возвратить 1, если результат
равен нулю; в противном случае возвратить О
atomic_add_negative (i, v) Прибавить i к *v и возвратить 1, если результат
отрицательный; в противном случае возвратить О
atomic_inc_return (v) Прибавить 1 к *v и возвратить новое значение *v
atomic_dec_return (v) Вычесть 1 из *v и возвратить новое значение *v
atomic_add_return(i, v) Прибавить i к *v и возвратить новое значение *v
atomic_sub_return (i, v) Вычесть i из *v и возвратить новое значение *v
Таблица 5.5. Атомарные функции Linux для работы с битами
Функция Описание
testbit (nr, addr) Возвратить значение nr-го бита *addr
set_bit (nr, addr) Установить nr-й бит *addr
clear_bit (nr, addr) Сбросить nr-й бит * addr
change_bit (nr, addr) Инвертировать nr-й бит *addr
test_and_set_bit (nr, addr) Установить nr-й бит *addr и возвратить его ста-
старое значение
test_and_clear_bit (nr, addr) Сбросить nr-й бит *addr и возвратить его старое
значение
test_and_change_bit (nr, addr) Инвертировать nr-й бит *addr и возвратить его
старое значение
atomic_clear_mask (mask, addr) Сбросить биты *addr, заданные с помощью mask
atomic_set_mask (mask, addr) Установить биты *addr, заданные с помощью
mask
Барьеры оптимизации и барьеры памяти
Работая с оптимизирующими компиляторами, вы не можете предполагать как
само собой разумеющееся, что операторы будут выполняться именно в том
порядке, в каком они идут в исходном коде. Например, компилятор может
переставить ассемблерные инструкции так, чтобы регистры использовались
оптимальным образом. Кроме того, современные процессоры обычно выпол-
выполняют несколько инструкций параллельно и могут изменить порядок обраще-
обращения к памяти. Такое переупорядочивание может сильно ускорить работу про-
программы.
Однако в тех случаях, когда дело касается синхронизации, переупорядочива-
переупорядочивания инструкций следует избегать. Если инструкция, расположенная после
примитива синхронизации, будет выполнена раньше этого примитива, пута-
путаница неизбежна. Поэтому все примитивы синхронизации действуют как
барьеры оптимизации и барьеры памяти.
Барьер оптимизации гарантирует, что ассемблерные инструкции, соответст-
соответствующие операторам языка С, помещенным до примитива, не будут переме-
перемешаны компилятором с инструкциями, соответствующими операторам язы-
языка С, поставленным после примитива. В Linux макрос barrier (), который
расширяется в asm volatile("":::"memory"), действует как барьер оптимиза-
ции. Инструкция asm заставляет компилятор вставить ассемблерный фраг-
фрагмент (в данном случае пустой). Ключевое слово volatile запрещает компиля-
компилятору переставлять инструкцию asm по отношению к другим инструкциям
программы. Ключевое слово memory вынуждает компилятор предполагать, что
все ячейки памяти были изменены ассемблерными инструкциями, и он по-
поэтому не может оптимизировать код, используя содержимое ячеек, сохранен-
сохраненное в регистрах процессора до выполнения инструкции asm. Обратите внима-
внимание: барьер оптимизации не гарантирует, что ассемблерные инструкции не
будут "перетасованы" процессором, это задача барьера памяти.
Барьер памяти гарантирует, что операции, помещенные до примитива, будут
завершены до начала выполнения операций, расположенных после примити-
примитива. Таким образом, барьер памяти работает как брандмауэр, сквозь который
не могут пройти ассемблерные инструкции.
В процессорах 80x86 следующие ассемблерные инструкции обеспечивают
"сериализацию", поскольку они действуют как барьеры памяти:
□ все инструкции, работающие с портами ввода/вывода;
□ все инструкции, имеющие байт lock в качестве префикса (см. разд. "Ато-
"Атомарные операции");
□ все инструкции, которые пишут в управляющие регистры, системные ре-
регистры и регистры отладки (например, инструкции cli и sti, изменяющие
состояние флага if в регистре ef lags);
□ ассемблерные инструкции l fence, s fence и mfence, появившиеся в микро-
микропроцессорах Pentium 4 для эффективной реализации барьеров чтения па-
памяти, записи в память и чтения/записи соответственно;
□ несколько специальных ассемблерных инструкций, в том числе инструк-
инструкция iret, завершающая обработчик прерывания или исключения.
В Linux применяется ряд барьеров памяти, которые перечислены в табл. 5.6.
Эти примитивы работают и как барьеры оптимизации, поскольку они должны
не позволить компилятору перемещать ассемблерные инструкции вокруг
барьера. Барьеры "чтения памяти" действуют только на инструкции, читаю-
читающие данные из памяти, а барьеры "записи в память" влияют только на инст-
инструкции, пишущие в память. Барьеры памяти могут быть полезны как в одно-
однопроцессорных, так и в многопроцессорных системах. Примитивы smpxxxo
применяются, когда барьер памяти должен предотвратить состояния гонки,
возникающие только в многопроцессорных системах; в однопроцессорных
системах эти инструкции эффекта не имеют. Остальные барьеры памяти ис-
используются для предотвращения конфликтов одновременного обращения,
возникающих как в однопроцессорных, так и в многопроцессорных системах.
Таблица 5.6. Барьеры памяти в Linux
Макрос Описание
mb () Барьер памяти для многопроцессорных и однопроцессорных систем
rmb() Барьер чтения из памяти для многопроцессорных и однопроцессорных
систем
wmb() Барьер записи в память для многопроцессорных и однопроцессорных
систем
smpmb () Барьер памяти только для многопроцессорных систем
smprrab () Барьер чтения из памяти только для многопроцессорных систем
smp_wmb () Барьер записи в память только для многопроцессорных систем
Реализация барьеров памяти зависит от архитектуры системы. В архи-
архитектуре 80x86 макрос ппьо обычно развертывается в оператор
asm volatile ("ifence"), если процессор поддерживает ассемблерную инст-
инструкцию lfence, ИЛИ В asm volatile("lock;addl $0,0(%%esp)"::: "memory")
в противном случае. Оператор asm вставляет ассемблерный фрагмент в код,
сгенерированный компилятором, и действует как барьер оптимизации. Ас-
Ассемблерная инструкция lock; addl $0,0(%%esp) прибавляет ноль к верхнему
элементу стека. Сама по себе она бесполезна, но префикс lock превращает ее
в барьер памяти для процессора.
Макрос wmbo, в принципе, проще, поскольку расширяется в макрос
barrier о. Это сделано, потому что существующие микропроцессоры Intel не
переупорядочивают операции записи в память, и, следовательно, нет необхо-
необходимости вставлять в код сериализующую ассемблерную инструкцию. Однако
этот макрос не позволяет компилятору перетасовывать инструкции.
Заметим, что в многопроцессорных системах все атомарные операции, опи-
описанные ранее в этой главе, работают барьерами памяти, поскольку имеют
префиксный байт lock.
Спин-блокировки
Одним из широко применяемых приемов синхронизации является блокиро-
блокирование. Когда управляющий тракт ядра должен обратиться к совместно ис-
используемой структуре или войти в критическую область, ему необходимо
получить "блокировку". Ресурс, защищенный блокировкой, аналогичен ре-
ресурсу, находящемуся в комнате, дверь которой заперта на замок, когда внут-
внутри кто-то находится. Если управляющий тракт ядра желает обратиться к ре-
ресурсу, он пытается "открыть дверь", иначе говоря, получить блокировку. Ему
это удается, только если ресурс свободен. Затем тракт пользуется ресурсом,
сколько ему нужно, а дверь остается запертой. Когда тракт освобождает бло-
блокировку, дверь отпирается, и другой тракт ядра может войти в комнату.
Применение блокировок проиллюстрировано на рис. 5.1. Пять трактов ядра
(РО, Р1, Р2, РЗ и Р4) пытаются обратиться к двум критическим областям (С1 и
С2). Тракт РО находится внутри области С1, а тракты Р2 и Р4 ждут своей оче-
очереди. В то же время тракт Р1 находится внутри области С2, а тракт РЗ ждет.
Обратите внимание, что тракты РО иР1 могут выполняться параллельно.
Замок критической области СЗ открыт, поскольку она не нужна никакому
тракту.
Рис. 5.1. Защита критических областей с помощью блокировок
Спин-блокировки— это специальный вид блокировок, разработанных для
использования в многопроцессорной среде. Если тракт ядра обнаруживает,
что спин-блокировка "открыта", он получает блокировку и продолжает свое
выполнение. Если же потоку становится известно, что блокировка "закрыта"
(захвачена) управляющим трактом ядра, работающим на другом процессоре,
он "крутится" на одном месте, выполняя "плотный" цикл, пока блокировка не
будет освобождена.
Цикл инструкций, выполняемых потоком при спин-блокировке, является
"ожиданием без прекращения выполнения". Ожидающий тракт ядра продол-
продолжает выполняться процессором, хотя он впустую тратит время. Тем не менее
спин-блокировки обычно оказываются очень удобными, поскольку многие
ресурсы ядра запираются всего лишь на долю миллисекунды, а на освобож-
освобождение процессора и последующее его получение у потока ушло бы гораздо
больше времени.
В качестве общего правила вытеснение в ядре отключается в каждой крити-
критической области, защищенной спин-блокировками. В однопроцессорной сие-
теме блокировки сами по себе бесполезны, и примитивы — спин-блоки-
спин-блокировки — просто отключают и включают вытеснение в ядре. Обратите внима-
внимание, что на этапе ожидания без прекращения выполнения вытеснение в ядре
еще не отключено, и процесс, ожидающий освобождения спин-блокировки,
может быть замещен процессом с более высоким приоритетом.
В Linux каждая спин-блокировка представлена структурой spiniockt, со-
состоящей из двух полей:
□ slock— кодирует состояние спин-блокировки: 1 соответствует открытой
спин-блокировке, а любое отрицательное значение и 0 — закрытой;
□ breakiock— флаг, сигнализирующий, что процесс ожидает освобожде-
освобождения блокировки без прекращения выполнения (присутствует, только если
ядро поддерживает как симметричную многопроцессорную обработку, так
и вытеснение в ядре).
Шесть макросов, приведенных в табл. 5.7, служат для инициализации, про-
проверки и установки спин-блокировок. Все они основаны на атомарных опера-
операциях, и это является гарантией, что спин-блокировка будет обновлена кор-
корректно, даже если несколько процессов, работающих на разных процессорах,
одновременно попытаются модифицировать его2.
Таблица 5.7. Макросы для спин-блокировок
Макрос Описание
spin_lock_init () Установить спин-блокировку в 1 (разблокировано)
spin_lock () Выполнять цикл, пока спин-блокировка не станет равной 1
(разблокировано), а затем сбросить ее в 0 (заблокировано)
spin_unlock () Установить спин-блокировку в 1 (разблокировано)
spin_unlock_wait () Ждать, пока спин-блокировка не станет равной 1 (разблокиро-
(разблокировано)
spin_is_locked () Возвратить 0, если спин-блокировка установлена в 1 (разбло-
(разблокировано); возвратить 1 в противном случае
spin_trylock () Сбросить спин-блокировку в 0 (заблокировано) и возвратить 1,
если предыдущим значением спин-блокировки была единица;
в противном случае возвратить О
Макрос spin Jock при наличии вытеснения в ядре
Сейчас мы подробно обсудим макрос spiniock, используемый для получе-
получения спин-блокировки. Последующее описание относится к вытесняющему
2 Забавно, что спин-блокировки, будучи глобальными структурами, сами нуждаются в защите от
попыток одновременного обращения.
ядру с поддержкой симметричной многопроцессорной обработки. Макрос
принимает в качестве параметра адрес спин-блокировки sip и выполняет сле-
следующие действия:
1. Вызывает функцию preemptdisabieo, чтобы отключить вытеснение
в ядре.
2. Вызывает функцию rawspintryiocko, которая выполняет атомарную опе-
операцию проверки и установки поля slock у данной спин-блокировки. Вна-
Вначале эта функция выполняет операторы, эквивалентные следующему
фрагменту ассемблерного кода:
movb $0, %al
xchgb %al, slp->slock
Ассемблерная инструкция xchg атомарно обменивает содержимое 8-би-
8-битового регистра %ai (в котором хранится ноль) и содержимое ячейки памя-
памяти, на которую указывает конструкция sip->siock. Функция возвращает 1,
если значение, которое хранилось в спин-блокировке (а после выполнения
инструкции xchg хранится в регистре %ai), было положительным, и 0 в
противном случае.
3. Если прежнее значение было положительным, макрос завершает работу:
управляющий тракт ядра получил спин-блокировку.
4. В противном случае управляющему тракту ядра не удалось получить
спин-блокировку, и, следовательно, макрос должен выполнять цикл, пока
спин-блокировка не будет освобождена трактом ядра, выполняющимся на
каком-то другом процессоре. Макрос вызывает функцию preempt_
enable (), чтобы отменить увеличение счетчика вытеснений, сделанное на
шаге 1. Если вытеснение в ядре было включено до выполнения макроса
spiniock, другой процесс может заместить данный, пока он дожидается
освобождения спин-блокировки.
5. Если поле breakiock равно нулю, макрос записывает в него единицу.
Проверяя это поле, процесс, владеющий спин-блокировкой и работающий
на другом процессоре, может узнать о наличии других процессов, ожи-
ожидающих спин-блокировку. Если процесс держит спин-блокировку доволь-
довольно долго, он может решить досрочно освободить ее, чтобы процессы,
ожидающие эту блокировку, могли продвинуться в своем выполнении.
6. Выполняет цикл ожидания:
while (spin_is_locked(slp) && slp->break_lock)
cpu_relax () ;
Макрос cpurelax () сводится к ассемблерной инструкции pause. Эта инст-
инструкция появилась в модели Pentium 4 для оптимизации циклов спин-
блокировок. Создавая небольшую задержку, она ускоряет выполнение ко-
кода, следующего после блокировки, и уменьшает потребление энергии. Ин-
Инструкция pause обладает обратной совместимостью с более ранними моде-
моделями микропроцессоров 80x86 , потому что она соответствует инструкции
rep;пор, т. е. пустой операции.
7. Возвращается к шагу 1, чтобы попытаться получить спин-блокировку еще
раз.
Макрос spinjock при отсутствии вытеснения в ядре
Если при компиляции ядра возможность вытеснения в ядре не была преду-
предусмотрена, макрос spiniock будет существенно отличаться от того, что опи-
описан в предыдущем разделе. В этом случае макрос возвращает фрагмент ас-
ассемблерного кода, фактически эквивалентный следующему "плотному" цик-
циклу ожидания без прекращения выполнения3:
1: lock; decb slp->slock
jns 3f
2: pause
cmpb $0,slp->slock
jle 2b
jmp lb
3:
Ассемблерная инструкция decb уменьшает значение спин-блокировки. Она
атомарна, потому что имеет префикс в виде байта lock. Затем проверяет флаг
знака. Если он сброшен, значит, спин-блокировка была установлен в 1 (раз-
(разблокировано), и нормальное выполнение продолжается с метки 3 (суффикс f
отмечает тот факт, что инструкция перехода направлена вперед, т. е. метка
появляется в одной из последующих строчек программы). В противном слу-
случае "плотный" цикл после метки 2 (суффикс ь означает метку, расположен-
расположенную "сзади") выполняется до тех пор, пока спин-блокировка не примет поло-
положительное значение. Затем выполнение продолжается с метки 1, потому что
небезопасно идти дальше, не убедившись, что блокировка не захвачена дру-
другим процессом.
Макрос spin_unlock
Макрос spinuniock освобождает спин-блокировку, полученную процессом.
Он выполняет следующую ассемблерную инструкцию:
movb $1, slp->slock
J Фактическая реализация этого цикла выглядит чуть сложнее. Код после метки 2, выполняемый
только если спин-блокировка занята, помещен в отдельную секцию, чтобы в самом вероятном слу-
случае (когда он свободен) аппаратный кэш не содержал код, который не будет выполняться. В нашем
обсуждении мы опускаем эти подробности оптимизации.
а затем вызывает функцию preemptenabie () (если вытеснение в ядре не под-
поддерживается, функция preemptenabie () ничего не предпринимает). Заметьте,
что байт lock не используется, потому что в микропроцессорах 80x86 обра-
обращения к памяти только для записи выполняются атомарно.
Спин-блокировки чтения/записи
Спин-блокировки чтения/записи были созданы для повышения уровня парал-
параллельности в ядре. Они позволяют нескольким управляющим трактам ядра
одновременно читать одну структуру, если никакой другой тракт не модифи-
модифицирует ее. Если какой-то тракт пожелает записать данные в структуру, он
должен будет захватить спин-блокировку на запись и получить исключи-
исключительный доступ к ресурсу. Естественно, параллельное чтение данных повы-
повышает производительность системы.
На рис. 5.2 изображены две критические области (С1 и С2), защищенные
спин-блокировками чтения/записи. Тракты ядра R0 и R1 одновременно чи-
читают данные в области С1, а тракт W0 ждет спин-блокировку на запись.
Тракт W1 пишет данные в область С2, а тракты R2 и W2 ждут спин-
блокировку на чтение и запись соответственно.
Рис. 5.2. Спин-блокировки чтения/записи
Каждая спин-блокировка чтения/записи представлена структурой rwiockt.
Ее поле lock имеет длину 32 бита и кодирует два разных информационных
элемента:
□ 24-битовый счетчик, показывающий количество трактов ядра, одновре-
одновременно читающих защищаемую структуру. Значения счетчика в дополни-
дополнительном коде хранится в битах 0—23 этого поля;
□ флаг "разблокировано", устанавливаемый, когда никакой тракт не читает и
не записывает данные, и сбрасываемые в противном случае. Этот флаг
хранится в бите 24.
Обратите внимание, что в поле lock хранится число Охоюооооо, если спин-
блокировка свободна (флаг "разблокировано" установлен, и нет читающих
трактов), число Охоооооооо, если спин-блокировка получена для записи (флаг
"разблокировано" сброшен, и нет читающих трактов), и любое число из по-
последовательности OxOOffffff, OxOOfffffe и т. д., если спин-блокировка была
получена для чтения одним, двумя и т. д. процессами (флаг "разблокировано"
сброшен, а в двадцати четырех битах хранится число читающих трактов в
дополнительном коде). Как и структура spiniockt, структура rwiockt имеет
ПОЛе break_lock.
Макрос rwiockinit инициализирует поле lock спин-блокировки чте-
чтения/записи значением Охоюооооо (разблокировано), а поле breakiock— ну-
нулем.
Получение и освобождение блокировки на чтение
Макрос readiock, принимающий в качестве параметра адрес rwip спин-
блокировки чтения/записи, аналогичен макросу spiniock, описанному ранее.
Если компиляции ядра была выбрана опция вытеснения в ядре, макрос вы-
выполняет точно те же действия, что макрос spiniock о, но с единственным
исключением: чтобы получить спин-блокировку чтения/записи на шаге 2,
макрос ВЫЗЫВает функцию rawreadtrylock () \
int rawreadtrylock(rwiockt *lock)
{
atomic_t *count = (atomic_t *)lock->lock;
atomic_dec(count);
if (atomic_read(count) >= 0)
return 1;
atomic_inc(count);
return 0;
}
Обращение к полю lock, счетчику спин-блокировки чтения/записи, происхо-
происходит с помощью атомарных операций. Однако следует обратить внимание, что
функция в целом работает со счетчиком не атомарно. Например, счетчик мо-
может измениться между проверкой его значения в операторе if и возвратом
единицы. Тем не менее функция работает корректно. В действительности,
она возвращает 1, только если счетчик не содержал ноль или отрицательное
число перед уменьшением, поскольку счетчик равен Охоюооооо, когда спин-
блокировка не захвачена, OxOOffffff, когда есть один читающий тракт, и
Охоооооооо, когда имеется один пишущий тракт.
Если при компиляции ядра опция вытеснения в ядре не была выбрана, макрос
readiock возвращает следующий ассемблерный код:
movl $rwlp->lock,%eax
lock; subl $1,(%еах)
jns If
call read lock failed
1:
Здесь readiockf ailed о представляет собой следующую функцию на ас-
ассемблере:
read_lock_failed:
lock; incl (%eax)
1: pause
cmpl $1,(%eax)
js lb
lock; decl (%eax)
js read_lock_failed
ret
Макрос readiock атомарным образом уменьшает значение спин-блокировки
на 1, тем самым увеличивая количество читающих потоков. Тракт получает
спин-блокировку, если операция уменьшения дает неотрицательное значение.
В противном случае вызывается функция read_iock_faiied(). Она атомар-
атомарно увеличивает значение поля lock, чтобы отменить уменьшение, выполне-
выполнение макросом readiock, а затем входит в цикл, пока поле не станет положи-
положительным (больше или равным 1). После этого функция readiockfaiiedo
снова пытается получить спин-блокировку (другой управляющий тракт ядра
мог получить спин-блокировку на запись сразу после инструкции cmpl).
Освобождение спин-блокировки, полученной для чтения, происходит очень
просто, поскольку макрос readuniock должен всего лишь увеличить счетчик
в поле lock следующей ассемблерной инструкцией:
lock; incl rwlp->lock
(уменьшая тем самым количество читающих потоков), а затем вызвать функ-
функцию preemptenabie (), чтобы вновь включить вытеснение в ядре.
Получение и освобождение блокировки на запись
Макрос writeiock реализован аналогично макросам spiniocko и
readiock (). Например, если поддерживается вытеснение в ядре, и пытается
Тут Же ПОЛуЧИТЬ СПИН-блОКИрОВКу, ВЫЗВаВ фуНКЦИЮ rawwritetrylock(). ЕСЛИ
вызванная функция возвращает 0, значит, спин-блокировка уже занята. Мак-
Макрос включает вытеснение в ядре и запускает цикл ожидания без прекращения
выполнения, как показано в описании макроса spiniock () в одном из преды-
предыдущих разделов.
Приведем КОД фуНКЦИИ rawwritetrylock ():
int rawwritetrylock(rwlockt *lock)
{
atomic_t *count = (atomic_t *)lock->lock;
if (atomic_sub_and_test@x01000000, count))
return 1;
atomic_add@x01000000, count);
return 0;
}
Функция rawwritetrylock о вычитает OxOioooooo из значения спин-
блокировки чтения/записи, чтобы сбросить флаг "разблокировано" (бит 24).
Если в результате вычитания получен ноль (нет читающих потоков), блоки-
блокировка выделяется, и функция возвращает 1. В противном случае функция
атомарно прибавляет OxOioooooo к значению спин-блокировки, чтобы отме-
отменить операцию вычитания.
И в этом случае освобождение спин-блокировки гораздо проще получения,
поскольку макрос writeuniock должен установить флаг "разблокировано" в
поле lock с помощью ассемблерной инструкции:
lock; addl $0x01000000,rwlp
а затем вызвать функцию preempt_enable ().
Seqlock-блокировки
Когда используются спин-блокировки чтения/записи, запросы, выдаваемые
трактами ядра на выполнение операции readiock или writeiock, имеют
одинаковый приоритет. Читающие тракты должны ждать, пока завершится
пишущий тракт, а пишущему тракту приходится ждать завершения всех чи-
читающих.
Seqlock-блокировки, введенные в Linux 2.6, аналогичны спин-блокировкам
чтения/записи, но предоставляют пишущим трактам намного более высокий
приоритет. Пишущему тракту разрешается работать даже при наличии ак-
активных читающих потоков. Положительная сторона этой стратегии состоит в
том, что пишущий тракт никогда не ждет (если нет другого активного пишу-
пишущего потока); отрицательная же заключается в том, что читающему тракту
иногда приходится несколько раз считывать одни и те же данные, пока он,
наконец, не получит действующую копию.
Каждая seqlock-блокировка представлена структурой seqiockt, которая со-
состоит из двух полей: поля lock, имеющего тип spiniockt, и целочисленного
поля sequence. Это второе поле играет роль счетчика порядковых номеров.
Каждый читающий тракт должен прочитать этот счетчик дважды, до и после
операции чтения данных, а затем убедиться, что оба значения совпадают. Ес-
Если это не так, значит, активизировался новый пишущий тракт, который уве-
увеличил счетчик, давая понять читающему тракту, что только что прочитанные
данные уже устарели.
Переменная seqiockt инициализируется в состояние "разблокировано", для
чего ей напрямую присваивается значение seqlockunlocked, либо вызывается
макрос seqiockinit. Пишущие тракты получают и освобождают seqlock-
блОКИрОВКу, ВЫЗЫВая функции write_seqlock() И writesequnlock (). Первая
функция захватывает спин-блокировку в структуре seqiockt, затем увеличи-
увеличивает на единицу счетчик порядковых номеров. Вторая функция увеличивает
счетчик еще раз и освобождает спин-блокировку. В результате получается,
что, когда пишущий тракт занимается своим делом, счетчик содержит нечет-
нечетное число, а когда трактов потоков нет, в счетчике хранится четное. Читаю-
Читающие тракты реализуют критическую область следующим образом:
unsigned int seq;
do {
seq = read_seqbegin(&seqlock);
/* ... CRITICAL REGION ... */
} while (read_seqretry(&seqlock, seq));
Функция readseqbegin () возвращает текущий порядковый номер seqlock-
блокировки. Функция readseqretryo возвращает 1, если либо значение ло-
локальной переменной seq нечетно (пишущий тракт обновлял данные, когда
была вызвана функция readseqbegin ()), либо значение seq не совпадает с
текущим значением счетчика порядковых номеров (пишущий тракт присту-
приступил к работе, когда читающий тракт все еще выполнял код критической об-
области).
Обратите внимание, что когда читающий тракт входит в критическую об-
область, он не должен отключать вытеснение в ядре. Зато пишущий тракт авто-
автоматически отключает вытеснение в ядре при входе в критическую область,
поскольку он захватывает спин-блокировку.
Не каждую структуру данных можно защищать seqlock-блокировкой. В каче-
качестве общего правила можно сформулировать следующие условия:
□ защищаемая структура не должна включать в себя указатели, которые мо-
модифицируются пишущими трактами и разыменуются читающими (в про-
противном случае какой-нибудь пишущий тракт сможет изменить указатель
"под носом" у читающих трактов);
□ код в критических областях читающих трактов не должен иметь побочных
эффектов (в противном случае многократное чтение даст не тот результат,
что однократное).
Кроме того, критические области читающих трактов должны быть коротки-
короткими, а пишущие тракты должны редко получать seqlock-блокировку. В про-
противном случае многократные попытки чтения потребуют непомерных на-
накладных расходов. Типичным применением seqlock-блокировок в Linux 2.6
является защита структур данных, связанных с хронометрированием (см. гла-
главу 6).
Обновление копии для чтения (RCU)
Обновление копии для чтения (Read-copy update, RCU) является еще одним
приемом синхронизации, разработанным для защиты структур, к которым
несколько процессоров обращается, в основном, для чтения. RCU позволяет
нескольким пишущим и читающим трактам работать одновременно (это шаг
вперед, по сравнению с seqlock-блокировками, позволяющими работать толь-
только одному пишущему тракту). Более того, техника обновления копии для
чтения свободна от блокировок, т. е. в ней нет блокировки или счетчика, ко-
которые бы совместно использовались всеми процессорами. Это огромное пре-
преимущество перед спин-блокировками чтения/записи и seqlock-блокировками,
которые требуют больших накладных расходов из-за снупинга и порчи строк
кэша.
Каким образом применение техники RCU позволяет достичь удивительных
результатов при синхронизации нескольких процессов без совместно исполь-
используемых структур данных? Основная идея состоит в ограничении области дей-
действия RCU:
□ С помощью RCU можно защищать только динамически выделенные дан-
данные, доступные через указатели.
□ Никакой управляющий тракт ядра не может "спать" внутри критической
области, защищенной с помощью RCU.
Когда управляющему тракту ядра необходимо прочитать структуру, защи-
защищенную с помощью RCU, он выполняет макрос rcureadiock (), эквивалент-
эквивалентный макросу preernptdisableO. Затем читающий тракт разыменовывает ука-
указатель на структуру и приступает к ее чтению. Как было сказано, читающий
тракт не может спать, пока не закончит читать структуру, а конец критиче-
критической области отмечен макросом rcureaduniock (), эквивалентным макросу
preempt_enable().
Поскольку читающий тракт почти ничего не делает для предотвращения
конфликтов одновременного обращения, мы вправе ожидать, что вся работа
перекладывается на пишущий тракт. Действительно, когда пишущему тракту
нужно обновить структуру, он разыменовывает указатель и делает копию
всей структуры. Затем он обновляет копию. Закончив эту операцию, пишу-
щий тракт изменяет указатель на структуру так, чтобы он ссылался на обнов-
обновленную копию. Поскольку изменение значения указателя является атомар-
атомарным действием, любой читающий или пишущий тракт видит либо старый
экземпляр структуры, либо новый, и никакая порча данных невозможна. Од-
Однако при таком подходе необходим барьер памяти для гарантии того, что об-
обновленный указатель будет виден на других процессорах только после моди-
модификации структуры данных. Такой барьер памяти неявно устанавливается,
если в комбинации с обновлением копии используется спин-блокировка,
предотвращающая параллельное выполнение пишущих трактов.
Реальная проблема с техникой обновления копии для чтения заключается в
том, что старая копия не может быть освобождена сразу после того, как пи-
пишущий тракт изменит указатель. На практике читающие тракты, работавшие
со структурой, когда пишущий тракт начал процедуру обновления, могут все
еще читать старую копию. Ее можно освободить только после того, как все
(потенциальные) читающие потоки на всех процессорах выполнят макрос
rcureaduniock (). Ядро требует, чтобы каждый потенциальный читающий
тракт выполнил этот макрос до того, как:
□ процессор выполнит переключение процессов;
□ процессор начнет выполнение процесса в режиме пользователя;
□ процессор перейдет к выполнению холостого цикла (см. главу 3).
В каждом из этих случаев мы говорим, что процессор перешел в состояние
покоя.
Пишущий тракт вызывает функцию caiircuo, чтобы избавиться от старой
копии структуры данных. Функция принимает в качестве параметров адрес
дескриптора rcuhead (который обычно встроен в освобождаемую структуру)
и адрес функции обратного вызова, которая должна быть вызвана, когда все
процессоры перейдут в состояние покоя. Эта функция обратного вызова, как
правило, освобождает старую копию структуры данных.
Функция caiircuo сохраняет в дескрипторе rcuhead адрес функции обрат-
обратного вызова и ее параметр, а затем заносит дескриптор в список функций об-
обратного вызова (отдельный у каждого процессора). Периодически каждый
тик таймера (см. разд. "Обновление статистики локального процессора" гла-
главы 6) ядро проверяет, перешел ли процессор в состояние покоя. Когда все
процессоры перейдут в состояние покоя, локальный тасклет, дескриптор ко-
которого хранится в процессорной переменной rcutaskiet, выполняет все
функции обратного вызова, перечисленные в списке.
Обновление копии для чтения является нововведением в Linux 2.6; оно ис-
используется в сетевом слое и в виртуальной файловой системе.
Семафоры
Мы уже говорили о семафорах в главе 1. В сущности, они реализуют прими-
примитив блокировки, позволяющий процессам находиться в состоянии ожидания,
пока не освободится необходимый ресурс.
Linux включает в себя два вида семафоров:
□ семафоры ядра, используемые управляющими трактами ядра;
□ семафоры межпроцессного взаимодействия, используемые процессами
режима пользователя.
В этом разделе мы все внимание уделим семафорам ядра, а семафоры меж-
межпроцессного взаимодействия будут описаны в главе 19.
Семафор ядра аналогичен спин-блокировке в том, что он не позволяет управ-
управляющему тракту ядра продолжать выполнение, если установлена блокировка.
Однако когда тракт ядра пытается обратиться к занятому ресурсу, защищен-
защищенному семафору ядра, соответствующий процесс приостанавливается. Процесс
снова принимает состояние "выполняемый" после освобождения ресурса. Та-
Таким образом, семафоры ядра могут быть получены только функциям, кото-
которым разрешено "спать", т. е. обработчики прерываний и функции отложенно-
отложенного выполнения не могут воспользоваться ими.
Семафор ядра является объектом типа struct semaphore, состоящим из сле-
следующих полей:
□ count — хранит значение типа atomict. Если оно больше 0, ресурс свобо-
свободен, т. е. к нему можно тут же обратиться. Если поле count равно 0, значит,
семафор закрыт, но никакой другой процесс не ждет защищенный ресурс.
Наконец, если значение count отрицательно, значит, ресурс недоступен и
как минимум один процесс ожидает его освобождения;
□ wait — хранит адрес очереди ожидания, включающей все спящие процес-
процессы, ожидающие ресурс на данный момент. Естественно, если поле count
больше или равно нулю, очередь пуста;
□ sleepers — хранит флаг, показывающий, имеются ли процессы, спящие
"на семафоре". Вскоре мы увидим этот флаг в действии.
ФуНКЦИИ init_MUTEX() И init_MUTEX_LOCKED () МОГуТ быть ВЫЗВаНЫ ДЛЯ ИНИ-
циализации семафора эксклюзивного доступа. Они устанавливают поле
count, соответственно, в значение 1 (свободный ресурс с эксклюзивным дос-
доступом) и 0 (занятый ресурс, предоставленный в данный момент процессу,
инициализирующему семафор). Макросы declaremutex и declare_mutex_
locked делают то же самое, но они, кроме прочего, статически выделяют пе-
переменную типа struct semaphore. Обратите внимание, что поле count сема-
семафора также может быть инициализировано произвольным положительным
числом п. В этом случае максимум п процессам разрешено одновременно об-
обратиться к ресурсу.
Получение и освобождение семафоров
Вначале мы обсудим, как освобождать семафор, потому что это намного
проще, чем получить его. Когда процесс хочет снять блокировку, установ-
установленную семафором ядра, он вызывает функцию up (). Она эквивалентна сле-
следующему фрагменту ассемблерного кода:
movl $sem->count,%есх
lock; incl (%ecx)
jg If
lea %ecx,%eax
pushl %edx
pushl %ecx
call up
popl %ecx
popl %edx
1:
Здесь up () — это функция на языке С:
attribute ((regparmC))) void up(struct semaphore *sem)
{
wake_up(&sem->wait);
}
Функция up () увеличивает поле count семафора *sem, а затем проверяет, стало
ли поле больше 0. Увеличение значения count и установка флага, проверяе-
проверяемого последующей инструкцией перехода, должны быть выполнены атомар-
атомарно, иначе какой-нибудь другой управляющий тракт ядра сможет обратиться к
этому полю, что приведет к катастрофе. Если count больше 0, значит, в оче-
очереди ожидания нет спящих процессов, и ничего делать не надо. В противном
случае вызывается функция up о, которая будит один спящий процесс. Об-
Обратите внимание, что функция up о получает параметр из регистра еах
(описание функции switchto () в главе 3).
Если же процесс хочет захватить семафор ядра, он вызывает функцию down ().
Реализация функции down о довольно сложна, но, в сущности, она эквива-
эквивалентна следующему коду:
down:
movl $sem->count,%ecx
lock; decl (%ecx);
jns If
lea %ecx, %eax
pushl %edx
pushl %ecx
call down
popl %ecx
popl %edx
1:
Здесь down () — это функция на языке С:
attribute ((regparmC))) void down(struct semaphore * sem)
{
DECLARE_WAITQUEUE(wait, current);
unsigned long flags;
current->state = TASK_UNINTERRUPTIBLE;
spin_lock_irqsave(&sem->wait.lock, flags);
add_wait_queue_exclusive_locked(&sem->wait, &wait);
sem->sleepers++;
for (;;) {
if (!atomic_add_negative(sem->sleepers-l, &sem->count)) {
sem->sleepers = 0;
break;
}
sem->sleepers = 1;
spin_unlock_irqrestore(&sem->wait.lock, flags);
schedule();
spin_lock_irqsave(&sem->wait.lock, flags);
current->state = TASKJJNINTERRUPTIBLE;
}
remove_wait_queue_locked(&sem->wait, &wait);
wake_up_locked(&sem->wait);
spin_unlock_irqrestore(&sem->wait.lock, flags);
current->state = TASK_RUNNING;
}
Функция downo уменьшает поле count семафора *sem, а затем проверяет, ста-
стало ли поле отрицательным. И в этом случае уменьшение значения и после-
последующая проверка должны быть выполнены атомарно. Если значение count
больше или равно 0, текущий процесс получает ресурс, и выполнение проте-
протекает нормально. В противном случае, когда значение count отрицательно,
процесс должен быть приостановлен. Содержимое некоторых регистров со-
сохраняется в стеке, после чего вызывается функция down ().
Функция down () изменяет состояние текущего процесса с taskrunning на
taskuninterruptible и помещает процесс в очередь ожидания семафора. Пе-
ред обращением к полям структуры semaphore функция получает спин-бло-
спин-блокировку sem->wait.iock, которая защищает очередь ожидания семафора (см.
главу 3) и отключает локальные прерывания. Обычно функции, работающие
с очередями ожидания, захватывают и освобождают соответствующую спин-
блокировку по мере необходимости, когда добавляют или удаляют элемент.
Однако функция down о использует спин-блокировку очереди ожидания
для защиты и других полей структуры semaphore, чтобы никакой процесс,
работающий на другом процессоре, не смог прочитать или модифицировать
их. С этой целью функция down () вызывает версии функций работы с оче-
очередями, имеющие суффикс locked. Эти версии предполагают, что спин-
блокировка уже получена до их вызова.
Основная задача функции down о состоит в том, чтобы приостановить те-
текущий процесс до освобождения семафора. Однако способ, которым она это
делает, достаточно сложен. Чтобы вам легче было разобраться в коде, помни-
помните, что поле sleepers семафора обычно содержит 0, если в очереди ожидания
семафора нет спящих процессов, и 1 в противном случае. Мы постараемся
разъяснить код, рассмотрев несколько типичных примеров.
□ Семафор mutex открыт (count равно 1, sleepers равно 0)— макрос down
лишь записывает 0 в поле count и переходит к следующей инструкции ос-
основной программы; таким образом, функция down () вообще не выполня-
выполняется.
□ Семафор mutex закрыт (count равно 0, sleepers равно 0) — макрос down
уменьшает значение count и вызывает функцию down о (count равно-1,
sleepers равно 0). На каждом шаге цикла функция проверяет, отрицатель-
отрицательно ли значение count (заметим, что поле count не меняется функцией
atomicaddnegativeO, ПОТОМу ЧТО На МОМент ВЫЗОВа функции ПОЛе
sleepers равно 0):
• если поле count отрицательно, функция вызывает функцию schedule (),
чтобы приостановить текущий процесс. Поле count по-прежнему рав-
равно-1, а поле sleepers становится равным 1. Впоследствии процесс во-
возобновляет работу внутри цикла и снова производит проверку;
• если поле count неотрицательно, функция сбрасывает поле sleepers
в ноль и выходит из цикла. Она пытается разбудить другой процесс в
очереди ожидания семафора (но в нашем сценарии очередь теперь пус-
пуста) и освобождает семафор. После выхода оба поля, count и sleepers,
равны 0, что и требуется, когда семафор закрыт, но никакой процесс
его не ждет.
□ Семафор mutex закрыт, есть другие спящие процессы (count равно-1,
sleepers равно 1)— макрос down уменьшает значение count и вызывает
функцию down о (count равно -2, sleepers равно 1). Функция временно
записывает 2 в поле sleepers и отменяет уменьшение, выполненное мак-
макросом down, прибавляя значение sieepers-i к полю count. В то же время
функция проверяет, является ли значение count по-прежнему отрицатель-
отрицательным (семафор мог быть освобожден удерживавшим его процессом непо-
непосредственно перед входом функции down () в критическую область):
• если поле count отрицательно, функция записывает 1 в поле sleepers и
вызывает функцию schedule о, чтобы приостановить текущий процесс.
Поле count по-прежнему равно-1, а поле sleepers становится рав-
равным 1;
• если поле count неотрицательно, функция сбрасывает поле sleepers в
ноль, пытается разбудить еще один процесс в очереди ожидания сема-
семафора и освобождает семафор. После выхода оба поля, count и sleepers,
равны 0. Эти значения кажутся ошибочными, поскольку имеются дру-
другие спящие процессы. Однако следует принять во внимание, что какой-
то процесс в очереди был разбужен. Этот процесс выполняет следую-
следующую итерацию ЦИКЛа, функция atomic_add_negative () ВЫЧИТает 1 ИЗ
count, возвращая этому полю значение -1. Кроме того, перед возвраще-
возвращением в состояние ожидания разбуженный процесс записывает 1 в поле
sleepers.
Итак, код корректно работает во всех случаях. Заметим, что функция
wakeupo в функции down () будит, самое большее, один процесс, потому
что спящие процессы в очереди ожидания являются эксклюзивными (см. гла-
главу 3).
Только обработчики исключений, особенно служебные процедуры систем-
системных вызовов, могут пользоваться функцией down (). Обработчикам прерыва-
прерываний и функциям отложенного выполнения нельзя вызывать функцию down (),
потому что она приостанавливает процесс, когда семафор занят. По этой
причине Linux предлагает программистам функцию downtryiocko, которую
можно без опасений вызывать из упомянутых асинхронных функций. Она
идентична функции down(), за исключением случая, когда ресурс занят.
В этой ситуации функция немедленно возвращает управление, не переводя
процесс в состояние ожидания.
Кроме того, существует функция downinterruptibleo, несколько отличаю-
отличающаяся от предыдущих. Она широко используется в драйверах устройств, по-
поскольку позволяет процессам, принявшим сигнал, пока они заблокированы
семафором, отказаться от операции "опустить семафор". Если спящий про-
процесс разбужен сигналом для получения необходимого ресурса, функция уве-
увеличивает поле count семафора и возвращает значение -eintr. С другой сторо-
стороны, если функция downinterruptibie () доходит до нормального завершения,
она возвращает 0. Таким образом, драйвер устройства может отменить опе-
операцию ввода/вывода, если возвращено значение -eintr.
В заключение отметим, что, поскольку процессы обычно находят семафоры в
открытом положении, семафорные функции оптимизированы именно для
этого случая. В частности, функция up о не выполняет инструкции перехода,
если очередь ожидания семафора пуста; аналогичным образом, функция
down () не выполняет инструкции перехода, если семафор открыт. Сложность
реализации семафора объясняется как раз усилиями разработчиков избежать
дорогостоящих операций в главной ветви выполнения программы.
Семафоры чтения/записи
Семафоры чтения/записи аналогичны спин-блокировкам чтения/записи, опи-
описанным ранее, с тем отличием, что ожидающий процесс приостанавливается,
а не "крутит" цикл, пока семафор не будет открыт.
Несколько управляющих трактов ядра могут одновременно получить сема-
семафор чтения/записи на чтение, но любой пишущий тракт должен иметь ис-
исключительный доступ к защищенному ресурсу. Поэтому семафор может
быть получен на запись, только когда никакой другой тракт ядра не держит
его ни на чтение, ни на запись. Семафоры чтения/записи повышают уровень
параллельности в ядре, тем самым повышая общую производительность сис-
системы.
Ядро обрабатывает все процессы, ожидающие семафора чтения/записи стро-
строго по принципу "первым вошел — первым вышел". Каждый читающий или
пишущий тракт ядра, обнаруживший, что семафор закрыт, ставится в конец
очереди ожидания семафора. Когда семафор освобождается, ядро анализиру-
анализирует процессы в начале очереди. Первый процесс пробуждается в любом слу-
случае. Если это пишущий процесс, другие процессы в очереди продолжают
спать. Если это читающий процесс, все следующие читающие процессы,
вплоть до первого пишущего, тоже пробуждаются и получают блокировку.
Читающие процессы, которые стоят в очереди после пишущего, продолжают
спать.
Каждый семафор чтения/записи представлен структурой rwsemaphore, со-
состоящей из следующих полей:
□ count— содержит два 16-битовых счетчика. Счетчик в старшем слове
хранит в дополнительном коде сумму количества не ожидающих пишу-
пишущих трактов (либо 0, либо 1) и количества ожидающих трактов ядра.
Счетчик в младшем слове кодирует сумму неожидающих читающих и
пишущих трактов;
□ waitlist — указывает на список ожидающих процессов. Каждый элемент
этого списка является структурой rwsemwaiter, включающей в себя указа-
тель на дескриптор спящего процесса и флаг, показывающий, для чтения
или записи нужен семафор этому процессу;
□ waitiock — спин-блокировка, используемая для защиты очереди ожида-
ожидания И самой структуры rw_semaphore.
Функция initrwsem () инициализирует структуру rwsemaphore, обнуляя поле
count, приводя спин-блокировку waitiock в состояние "разблокировано" и
инициализируя список waitlist пустым списком.
Функции downreado и downwrite() получают семафор чтения/записи на
чтение и на запись соответственно. Аналогичным образом, функции
upread () и upwrite () освобождают семафор чтения/записи, занятый процес-
процессом. Функции down_read_trylock() И down_write_trylock () аналогичны фуНК-
циям downread () и downwrite () соответственно, но они не блокируют про-
процесс, если семафор занят. Наконец, функция downgradewrite() атомарно
трансформирует блокировку для записи в блокировку для чтения. Реализация
этих пяти функций имеет довольно объемистый код, но в нем легко разо-
разобраться, т. к. он аналогичен реализации обычных семафоров. Поэтому мы не
станем описывать эти функции.
Completion-блокировки
В Linux 2.6 имеется еще один примитив синхронизации, аналогичный сема-
семафорам. Это completion-блокировки. Они были введены для разрешения не-
нетривиальной конфликтной ситуации, возникающей в многопроцессорных
системах, когда процесс А выделяет временную семафорную переменную,
инициализирует ее как закрытый MUTEX-семафор, передает ее адрес про-
процессу В, а затем вызывает для нее функцию down (). Процесс А планирует
уничтожить семафор, как только будет разбужен. Затем процесс В, работаю-
работающий на другом процессоре, вызывает функцию up () для семафора. Однако
при теперешней реализации функций up о и down о они могут быть парал-
параллельно выполнены на одном семафоре. Получается, что процесс А может
быть разбужен и уничтожит временный семафор, пока процесс В выполняет
функцию up (). В результате функция up () попытается обратиться к несуще-
несуществующей структуре.
Конечно, можно было изменить реализацию функций down о и up (), запретив
их параллельное выполнение на одном семафоре. Однако пришлось бы до-
добавлять в код дополнительные инструкции, а этого следует избегать, когда
функции используются настолько интенсивно.
Completion-блокировка — это примитив синхронизации, разработанный спе-
специально для решения этой проблемы. Структура completion включает в себя
голову очереди и флаг:
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
Функция, соответствующая семафорной функции up о, называется
complete (). Она принимает в качестве параметра адрес структуры completion,
вызывает функцию spiniockirqsaveo для спин-блокировки очереди ожи-
ожидания completion-блокировки, увеличивает поле done, будит эксклюзивный
процесс, спящий в очереди wait, и, наконец, вызывает функцию
spin_unlock_irqrestore().
ФуНКЦИЯ, Соответствующая фуНКЦИИ down (), Называется wait_for_completion ().
Она принимает в качестве параметра адрес структуры completion и проверяет
значение флага done. Если оно больше нуля, функция waitforcompietiono
заканчивает работу, потому что функция complete () уже была выполнена на
другом процессоре. В противном случае функция waitf orcompletion () ста-
ставит процесс current в конец очереди в качестве эксклюзивного процесса и
переводит его в "спящее" состояние taskuninterruptible. Пробудившись,
функция удаляет процесс current из очереди. Затем она проверяет значение
флага done. Если оно равно нулю, функция заканчивает работу; в противном
случае процесс снова приостанавливается. Как и функция complete о, функ-
функция waitf orcompletion () пользуется спин-блокировкой очереди ожидания
completion-блокировки.
Основная разница между completion-блокировками и семафорами состоит в
том, как используется спин-блокировка, входящая в состав очереди ожида-
ожидания. В completion-блокировках спин-блокировка служит для гарантии того,
ЧТО фуНКЦИИ complete () И waitf orcompletion () не будут ВЫПОЛНЯТЬСЯ Па-
раллельно. В семафорах спин-блокировка применяется, чтобы не позволить
параллельно выполняющимся функциям down () внести путаницу в структуру
semaphore.
Отключение локальных прерываний
Отключение прерываний является одним из главных механизмов обеспече-
обеспечения того, что последовательность операторов ядра будет трактоваться как
критическая область. Такой подход позволяет управляющему тракту ядра
продолжить выполнение, даже когда аппаратура посылает сигналы IRQ, и
поэтому является эффективным способом защиты структур данных, к кото-
которым также обращаются обработчики прерываний. Однако само по себе от-
отключение локальных прерываний не защитит данные от параллельных обра-
обращений со стороны обработчиков прерываний, выполняемых на других про-
цессорах. Поэтому в многопроцессорных системах отключение локальных
прерываний часто сочетается с применением спин-блокировок (см. разд. "Син-
"Синхронизация обращений к структурам данных ядра" далее в этой главе).
Макрос locaiirqdisableO, в котором используется ассемблерная ин-
инструкция cli, отключает прерывания на локальном процессоре. Макрос
locaiirqenabie (), использующий инструкцию sti, включает их. Как было
сказано в главе 4, инструкции cli и sti сбрасывают и устанавливают, соот-
соответственно, флаг if в управляющем регистре efiags. Макрос irqsdisabiedo
возвращает единицу, если флаг if в регистре efiags сброшен.
Когда ядро входит в критическую область, оно отключает прерывания, сбра-
сбрасывая флаг if в регистре efiags. Однако при выходе из критической области
ядро часто не может просто взять и установить флаг. Прерывания могут об-
обрабатываться вложенным образом, и ядро не всегда знает, каким был флаг if
до начала выполнения текущего тракта. В таких случаях тракт должен сохра-
сохранять старое значение флага и восстанавливать его в конце своей работы.
Сохранение и восстановление содержимого регистра efiags выполняется с
ПОМОЩЬЮ макросов local_irq_save И localirqrestore (соответственно).
Макрос localirqsave копирует содержимое регистра efiags в локальную
переменную, после чего флаг if сбрасывается инструкцией cli. В конце кри-
критической области макрос localirqrestore восстанавливает первоначальное
содержимое регистра efiags. Таким образом, прерывания включаются, толь-
только если они были включены до того, как управляющий тракт ядра выдал ас-
ассемблерную инструкцию cli.
Запрет и разрешение функций
отложенного выполнения
В главе 4 мы говорили, что функции отложенного выполнения могут быть
выполнены в непредсказуемые моменты (фактически, по окончании обработ-
обработчиков аппаратных прерываний). Следовательно, структуры, к которым обра-
обращаются функции отложенного выполнения, должны быть защищены от по-
попыток одновременного обращения.
Тривиальным способом запрещения функций отложенного выполнения на
процессоре является отключение на нем прерываний. Поскольку обработчики
прерываний не выполняются, softirq-функции не могут быть вызваны асин-
асинхронно.
Однако, как мы увидим в следующем разделе, иногда ядру нужно запретить
выполнение функций отложенного выполнения, не отключая прерывания.
Локальные функции отложенного выполнения могут быть разрешены или
запрещены на локальном процессоре путем записи соответствующего значе-
ния в счетчик softirq-функций, который хранится в поле preemptcount деск-
дескриптора threadinf о текущего процесса.
Вспомним, что функция dosoftirqo не станет выполнять softirq-функцию,
если значение счетчика положительно. Более того, тасклеты реализованы на
основе softirq-функций, и поэтому установка счетчика в положительное зна-
значение запрещает выполнение всех функций отложенного выполнения на дан-
данном процессоре, а не только softirq-функций.
Макрос locaibhdisabie прибавляет единицу к счетчику softirq-функций ло-
локального процессора, а макрос locaibhenabie () вычитает единицу из этого
счетчика. Таким образом, ядро может сделать несколько вложенных вызовов
макроса locaibhdisabie. Выполнение функций отложенного выполнения
будет разрешено снова только макросом locaibhenabie, соответствующим
первому макросу local_bh_disable.
Уменьшив значение счетчика softirq-функций, макрос locaibhenabie () вы-
выполняет две важные операции, обеспечивающие своевременное выполнение
потоков, ожидающих своей очереди долгое время:
1. Проверяет счетчики аппаратных прерываний и softirq-функций в поле
preeinptcount локального процессора. Если оба они равны нулю, и имеют-
имеются softirq-функций, ожидающие выполнения, макрос вызывает функцию
dosoftirqo для их активизации (см. главу 4).
2. Проверяет, установлен ли флаг tifneedresched у локального процессора.
Если установлен, значит, "висит" запрос на переключение процессов.
В этом случае макрос вызывает функцию preemptscheduie () (см. разд. "Вы-
"Вытеснение в ядре"ранее в этой главе).
Синхронизация обращений
к структурам данных ядра
Совместно используемая структура данных может быть защищена от попы-
попыток одновременного обращения с помощью примитивов синхронизации, опи-
описанных в предыдущих разделах. Конечно, выбор примитива синхронизации
может существенно повлиять на производительность системы. Обычно раз-
разработчики ядра придерживаются следующего правила: всегда следует стре-
стремиться к максимальному уровню параллельности в системе.
В свою очередь, уровень параллельности зависит от двух основных факторов:
П от количества устройств ввода/вывода, работающих одновременно;
□ от количества процессоров, выполняющих производительную работу.
Для увеличения производительности ввода/вывода следует отключать преры-
прерывания лишь на короткие промежутки времени. Как было сказано в главе 4,
когда прерывания отключены, программируемый контроллер прерываний
временно игнорирует IRQ-запросы, выдаваемые устройствами ввода/вывода,
и никакие операции с этими устройствами не могут начаться.
Чтобы процессоры были использованы эффективно, следует, по возможно-
возможности, избегать примитивов синхронизации, основанных на спин-блокировках.
Когда процессор выполняет "плотный" цикл инструкций в ожидании освобо-
освобождения спин-блокировки, он впустую расходует драгоценные машинные
циклы. Хуже того, как мы уже говорили, спин-блокировки оказывают нега-
негативное воздействие на производительность системы в целом из-за их влияния
на аппаратные кэши.
Рассмотрим пару случаев, в которых можно достичь синхронизации, обеспе-
обеспечивая высокий уровень параллельности:
□ совместно используемую структуру, состоящую из единственного целого
значения, можно обновлять, если объявить ее с типом atomict и приме-
применять к ней атомарные операции. Атомарная операция работает быстрее,
чем спин-блокировки и отключение прерываний. Она замедляет только
управляющие тракты ядра, одновременно обращающиеся к структуре;
□ занесение элемента в совместно используемый связный список никогда не
бывает атомарным действием, потому что включает в себя, как минимум,
два обновления указателей. Тем не менее ядро иногда может выполнить
вставку элемента без применения блокировок и отключения прерываний.
В качестве примера, объясняющего, почему это работает, мы рассмотрим
ситуацию, в которой служебная процедура системного вызова (см.
разд. "Обработчик системного вызова и служебные процедуры"главы 10)
заносит новые элементы в однонаправленный список, а обработчик пре-
прерываний или функция отложенного выполнения асинхронно просматрива-
просматривает этот список.
На языке С вставка элемента реализуется следующими операторами при-
присваивания:
new->next = list_element->next;
list_element->next = new;
На ассемблере вставка элемента сводится к двум атомарным инструкциям,
идущим друг за другом. Первая устанавливает указатель next элемента
new, но не модифицирует список. Таким образом, если обработчик преры-
прерываний просмотрит список между выполнением первой и второй инструк-
инструкцией, он не увидит там нового элемента. Если же обработчик прерываний
обратится к списку после выполнения второй инструкции, новый элемент
уже будет в списке. Важно то, что в любом случае поддерживается непро-
тиворечивость и целостность списка. Однако эти его свойства сохраняют-
сохраняются, только если обработчик прерываний не модифицирует список. В про-
противном случае указатель next, только что установленный у элемента new,
может стать некорректным.
Разработчики не должны допустить, чтобы порядок следования этих двух
операций присваивания был изменен компилятором или управляющим
блоком процессора. В противном случае, если служебная процедура сис-
системного вызова будет прервана между двумя присваиваниями, обработчик
прерываний обнаружит испорченный список. Следовательно, нужно уста-
установить барьер записи в память:
new->next = list_element->next;
wmb () ;
list_element->next = new;
Выбор между спин-блокировками, семафорами
и отключением прерываний
К сожалению, схемы обращения к большинству структур ядра гораздо слож-
сложнее простых примеров, рассмотренных в предыдущем разделе, и разработчи-
разработчикам ядра приходится применять семафоры, спин-блокировки, отмену преры-
прерываний и запрет softirq-функций. Вообще говоря, выбор примитива синхрони-
синхронизации зависит от того, какие управляющие тракты ядра обращаются к
структуре (табл. 5.8). Не будем забывать, что, как только управляющий тракт
ядра получает спин-блокировку (а также блокировку чтения/записи, seqlock-
блокировку или блокировку чтения вида "обновление копии для чтения"),
отменяет локальные прерывания или запрещает локальные softirq-функций,
автоматически отключается вытеснение в ядре.
Таблица 5.8. Защита, необходимая структурам, к которым обращаются
управляющие тракты ядра
Защита Дополнительная защита
Управляющие тракты ядра в однопроцессорной в многопроцессорной
системе системе
Исключения Семафор Нет
Прерывания Отключение локальных Спин-блокировка
прерываний
Функции отложенного выпол- Нет Ничего или спин-
нения блокировка (см. табл. 5.9)
Исключения + прерывания Отключение локальных Спин-блокировка
прерываний
Таблица 5.8 (окончание)
Защита Дополнительная защита
Управляющие тракты ядра в однопроцессорной в многопроцессорной
системе системе
Исключения + функции Запрет локальных softirq- Спин-блокировка
отложенного выполнения функций
Прерывания + функции Отключение локальных Спин-блокировка
отложенного выполнения прерываний
Исключения + прерывания + Отключение локальных Спин-блокировка
функции отложенного прерываний
выполнения
Защита структуры,
к которой обращаются обработчики исключений
Если к структуре обращаются только обработчики исключений, конфликты
одновременного обращения легко понять и предотвратить. Самыми распро-
распространенными исключениями, вызывающими проблемы синхронизации, яв-
являются служебные процедуры системных вызовов (см. разд. "Обработчик
системного вызова и служебные процедуры" главы 10). При их выполнении
процессор работает в режиме ядра, обслуживая программу режима пользова-
пользователя. Таким образом, структура, к которой обращаются только обработчики
исключений, обычно представляет ресурс, который может быть назначен од-
одному или нескольким процессам.
Конфликты одновременного обращения предотвращаются с помощью сема-
семафоров, потому что эти примитивы позволяют процессам спать, пока ресурс
не станет доступным. Обратите внимание, что семафоры работают одинаково
хорошо как в однопроцессорных, так и в многопроцессорных системах.
Вытеснение в ядре не создает проблем. Если процесс, обладающий семафо-
семафором, вытесняется, новый процесс, работающий на том же процессоре, может
попытаться получить семафор. Если это произойдет, новый процесс будет
переведен в состояние сна, а старый, в конце концов, освободит семафор.
Единственным случаем, в котором вытеснение в ядре должно быть явно от-
отключено, является обращение к процессорным переменным. Это обсужда-
обсуждалось ъразд. "Процессорные переменные"ранее в этой главе.
Защита структуры,
к которой обращаются обработчики прерываний
Предположим, к некоторой структуре обращается только "верхняя половина"
обработчика прерываний. Из главы 4 мы узнали, что каждый обработчик пре-
рываний сериализован по отношению к самому себе, т. е. он не может вы-
выполняться параллельно сам себе. Следовательно, обращение к структуре не
требует применения примитивов синхронизации.
Однако картина меняется, если к структуре обращаются несколько обработ-
обработчиков прерываний. Один обработчик может прервать другой, а в многопро-
многопроцессорных системах разные обработчики могут выполняться параллельно.
Без синхронизации совместная структура данных может быть легко испор-
испорчена.
В однопроцессорных системах конфликты нужно предотвращать отключени-
отключением прерываний во всех критических областях обработчика прерываний. Ни-
Никакие менее радикальные меры не помогут, потому что другие примитивы
синхронизации не справятся с задачей. Семафор может блокировать процесс,
поэтому его нельзя применять в обработчике прерываний. Спин-блокировка,
наоборот, может "подвесить" систему: если обработчик прерываний, обра-
обратившийся к структуре, сам будет прерван, он не сможет освободить блоки-
блокировку, а новый обработчик будет ждать окончания "плотного" цикла спин-
блокировки.
Как всегда, в многопроцессорных системах требования еще выше. Конфлик-
Конфликтов одновременного обращения нельзя избежать простым отключением ло-
локальных прерываний. Действительно, если прерывания отключены на одном
процессоре, обработчики прерываний могут быть выполнены на других. Са-
Самым удобным способом предотвращения конфликтов является отключение
локальных прерываний (чтобы другие обработчики прерываний, выполняе-
выполняемые на том же процессоре, не мешали) в комбинации с захватом спин-
блокировки чтения/записи, защищающей структуру данных. Обратите вни-
внимание, что эти дополнительные спин-блокировки не "подвесят" систему,
потому что, даже если обработчик прерываний обнаружит, что спин-бло-
спин-блокировка занята, обработчик прерываний на другом процессоре, владеющий
спин-блокировкой, в конце концов, освободит ее.
В ядре Linux применяется ряд макросов, которые комбинируют отключение и
включение локальных прерываний с получением и освобождением спин-
блокировок. Все они приведены в табл. 5.9. В однопроцессорных системах
эти макросы лишь включают и отключают локальные прерывания и вытесне-
вытеснение в ядре.
Таблица 5.9. Макросы для работы со спин-блокировками с учетом прерываний
Макрос Содержание
spin_lock_irqA) local_irq_disable(); spin_lockA)
spin_unlock_irq A) spin_unlock A) ; local__irq_enable ()
Таблица 5.9 (окончание)
Макрос Содержание
spin_lock_bhA) local_bh_disable(); spin_lockA)
spin_unlock_bhA) spin_unlockA); local_bh_enable()
spin_lock_irqsave(l,f) local_irq_save(f); spin_lock(l)
spin_unlock_irqrestoreA,f) spin_unlockA); local_irq_restore(f)
read_lock_irqA) local_irq_disable(); read_lockA)
read_unlock_irqA) read_unlockA); local_irq_enable()
read_lock_bhA) local_bh_disable(); read_lockA)
read_unlock_bhA) read_unlockA); local_bh_enable()
write_lock_irqA) local_irq_disable(); write_lockA)
write_unlock_irqA) write_unlockA); local_irq_enable()
write_lock_bhA) local_bh_disable(); write_lockA)
write_unlock_bhA) write_unlockA); local_bh_enable()
read_lock_irqsaveA,f) local_irq_save(f); read_lockA)
read_unlock_irqrestoreA,f) read_unlock(l); local_irq_restore(f)
write_lock_irqsaveA,f) local_irq_save(f); write_lockA)
write_unlock_irqrestoreA, f) write_unlock(l); local_irq_restore(f)
read_seqbegin_irqsaveA,f) local_irq_save(f); read_seqbeginA)
read_seqretry_irqrestore(l,v,f) read_seqretry(l,v); local_irq_restore(f)
write_seqlock_irqsaveA,f) local_irq_save(f); write_seqlockA)
write_sequnlock_irqrestoreA,f) write_sequnlockA); local_irq_restore(f)
write_seqlock_irqA) local_irq_disable(); write_seqlockA)
write_sequnlock_irqA) write_sequnlockA); local_irq_enable()
write_seqlock_bhA) local_bh_disable(); write_seqlockA);
write_sequnlock_bhA) write_sequnlockA); local_bh_enable()
Защита структуры, к которой обращаются
функции отложенного выполнения
Какой тип защиты требуется для данных, используемых только функциями
отложенного выполнения? Это, в основном, зависит от вида самой функции.
В главе 4 мы пояснили, что softirq-функции и тасклеты принципиально отли-
отличаются уровнем параллельности.
Прежде всего, в однопроцессорных системах не может быть конфликтов од-
одновременного обращения. Дело в том, что выполнение функций отложенного
выполнения всегда сериализуется на процессоре, т. е. функция отложенного
выполнения не может быть прервана другой такой функцией. Следовательно,
никакие примитивы синхронизации и не требуются.
В многопроцессорных системах, наоборот, конфликты возможны, потому что
несколько функций отложенного выполнения могут работать одновременно.
В табл. 5.10 перечислены все возможные случаи.
Таблица 5.10. Защита, необходимая структурам, к которым обращаются
функции отложенного выполнения, в многопроцессорных системах
Функции отложенного выполнения Защита
Softirq-функции Спин-блокировка
Один тасклет Нет
Несколько тасклетов Спин-блокировка
Структура данных, к которой обращается softirq-функция, должна быть все-
всегда защищена, как правило, с помощью спин-блокировки, потому что одна и
та же softirq-функция может одновременно работать на двух и более процес-
процессорах. Структура, к которой обращается только один вид тасклетов, наобо-
наоборот, в защите не нуждается, потому что тасклеты одного вида не могут вы-
выполняться параллельно. Если же к структуре обращаются тасклеты несколь-
нескольких видов, ее защищать необходимо.
Защита структуры, к которой обращаются
обработчики исключений и прерываний
Рассмотрим структуру, к которой обращаются обработчики как исключений
(например, служебные процедуры системных вызовов), так и прерываний.
В однопроцессорных системах предотвратить конфликты одновременного
обращения довольно просто, поскольку обработчики прерываний не являют-
являются реентерабельными и не могут быть прерваны обработчиками исключений.
Если ядро обращается к структуре при отключенных локальных прерывани-
прерываниях, его код не может быть прерван. Если же к структуре обращается обработ-
обработчик прерываний только одного вида, он может не отключать локальные пре-
прерывания.
В многопроцессорных системах мы должны принимать во внимание парал-
параллельное выполнение обработчиков исключений и прерываний на других про-
процессорах. Отключение локальных прерываний должно сочетаться с примене-
применением спин-блокировок, чтобы заставить параллельно выполняющиеся тракты
ядра ждать, пока обработчик, обратившийся к структуре, закончит свою ра-
работу.
Иногда бывает предпочтительнее заменить спин-блокировку семафором. По-
Поскольку обработчики прерываний не могут быть приостановлены, они долж-
должны получить семафор с использованием "плотного" цикла и функции
downtryiock (). Для них семафор работает практически как спин-блокировка.
С другой стороны, служебные процедуры системных вызовов могут приоста-
приостановить вызвавший их процесс, если семафор занят. Для большинства систем-
системных вызовов это нормальное поведение. В этом случае семафоры предпочти-
предпочтительнее спин-блокировок, поскольку они повышают уровень параллельности
в системе.
Защита структуры,
к которой обращаются обработчики исключений
и функции отложенного выполнения
Структуру данных, к которой обращаются как обработчики исключений, так
и функции отложенного выполнения, можно воспринимать как структуру, к
которой обращаются обработчики исключений и прерываний. Функции от-
отложенного выполнения фактически активизируются в результате прерыва-
прерываний, и никакое исключение не может быть возбуждено, пока выполняется
функция отложенного выполнения. Следовательно, достаточно комбинации
отключения локальных прерываний с получением спин-блокировки.
На самом деле, этого более чем достаточно. Обработчик исключения может
вместо отключения локальных прерываний просто запретить функции отло-
отложенного выполнения при помощи макроса iocai_bh_disabie() (см. главу 4).
Запрещение только функций отложенного выполнения предпочтительнее от-
отключения прерываний, поскольку последние продолжают обслуживаться
процессором. Выполнение функций отложенного выполнения сериализовано
на каждом процессоре, так что конфликты не возникают.
Как обычно, в многопроцессорных системах необходимо применять спин-
блокировки для гарантии того, что к структуре в любой момент времени об-
обращается не больше одного управляющего тракта ядра.
Защита структуры, к которой обращаются обработчики
прерываний и функции отложенного выполнения
Этот случай аналогичен случаю со структурой, к которой обращаются обра-
обработчики прерываний и исключений. Прерывание может быть возбуждено во
время выполнения функции отложенного выполнения, функция не может ос-
остановить обработчик прерываний. Следовательно, конфликты следует пре-
предотвращать отключением локальных прерываний на время работы функции
отложенного выполнения. Однако обработчик прерываний может беспрепят-
ственно обратиться к структуре, к которой обратилась функция отложенного
выполнения без отключения прерываний, при условии, что никакой другой
обработчик прерываний не обращается к этой структуре.
И опять, в многопроцессорных системах необходимо применять спин-бло-
спин-блокировки для предотвращения одновременных попыток обратиться к структу-
структуре со стороны нескольких процессоров.
Защита структуры,
к которой обращаются обработчики исключений
и прерываний, а также функций отложенного выполнения
Как и в предыдущих случаях, отключение локальных прерываний и получе-
получение спин-блокировки почти всегда необходимы для предотвращения кон-
конфликтов. Обратите внимание, что здесь нет необходимости явно запрещать
выполнение функций отложенного выполнения. Поскольку они фактически
активизируются по окончании выполнения обработчиков прерываний, от-
отключения локальных прерываний вполне достаточно.
Примеры предотвращения конфликтов
Предполагается, что разработчики ядра осознают и решают проблемы син-
синхронизации, возникающие вследствие чередования управляющих трактов
ядра. Однако предотвращение конфликтов является сложной задачей, тре-
требующей четкого понимания того, как взаимодействуют различные компонен-
компоненты ядра. Чтобы дать читателю представление о том, что в действительности
происходит внутри ядра, мы рассмотрим несколько типичных примеров ис-
использования примитивов синхронизации, описанных в этой главе.
Счетчики ссылок
Счетчики ссылок широко применяются в ядре для предотвращения конфлик-
конфликтов, возникающих из-за одновременного выделения и освобождения ресурса.
Счетчик ссылок — это всего лишь счетчик типа atomict, ассоциированный с
некоторым ресурсом, таким как страница памяти, модуль или файл. Счетчик
атомарно увеличивается, когда управляющий тракт ядра приступает к ис-
использованию ресурса, и уменьшается, когда он заканчивает пользоваться ре-
ресурсом. Когда счетчик ссылок становится равным нулю, ресурсом никто не
пользуется, и его можно освободить, если это необходимо.
Глобальная блокировка ядра
В ранних версиях Linux широко применялась глобальная блокировка ядра
(известная также как большая блокировка ядра, Big Kernel Lock). В Linux 2.0
это была довольно жесткая спин-блокировка, в результате которой только
один процессор мог работать в режиме ядра в данный момент времени. Код
ядер 2.2 и 2.4 был намного более гибким и больше не полагался на единст-
единственную спин-блокировку. Разнообразные структуры данных ядра были за-
защищены большим количеством различных спин-блокировок. В ядре Linux 2.6
глобальная блокировка ядра применяется для защиты старого кода (в основ-
основном, функций, имеющих отношение к виртуальной файловой системе и к не-
нескольким конкретным файловым системам).
Начиная с версии 2.6.11, глобальная блокировка ядра реализуется семафором
по имени kerneisem (в предыдущих подверсиях версии 2.6 глобальная бло-
блокировка ядра была реализована с помощью спин-блокировки). Впрочем, гло-
глобальная блокировка ядра явление несколько более сложное, чем обычный
семафор.
Каждый дескриптор процесса включает в себя поле lockdepth, позволяющее
одному процессу получать глобальную блокировку ядра несколько раз. Та-
Таким образом, два последовательных запроса на блокировку не подвесят про-
процессор (как в случае с обычными блокировками). Если процессор не получил
блокировку, это поле имеет значение -1; в противном случае значение поля,
увеличенное на единицу, показывает, сколько раз была получена блокировка.
Поле lockdepth играет принципиальную роль при разрешении обработчикам
прерываний, обработчикам исключений и функциям отложенного выполне-
выполнения захватить глобальную блокировку ядра. Без этого поля любая асинхрон-
асинхронная функция, пытающаяся получить глобальную блокировку ядра, могла бы
создать взаимную блокировку, если у текущего процесса уже есть блоки-
блокировка.
ФуНКЦИИ lockkernelO И unlockkernel () ИСПОЛЬЗуЮТСЯ ДЛЯ получения И ОС-
вобождения глобальной блокировки ядра. Первая функция эквивалентна та-
такому коду:
depth = current->lock_depth + 1;
if (depth == 0)
down(&kernel sem) ;
current->lock_depth = depth;
а вторая — такому:
if (—current->lock_depth < 0)
up(&kernel_sem);
Обратите внимание, что нет необходимости в атомарном выполнении опера-
операторов if В фунКЦИЯХ lockkernelO И unlockkernel (), ПОТОМу ЧТО ПОЛе
lockdepth не является глобальной переменной: каждый процессор обращает-
ся к полю дескриптора своего текущего процесса. Локальные прерывания по
ходу выполнения операторов if тоже не приведут к конфликтам одновремен-
одновременного обращения. Даже если новый управляющий тракт ядра вызовет функ-
функцию lockkernei (), он должен будет освободить блокировку перед заверше-
завершением работы.
Достаточно неожиданный факт: процесс, обладающий глобальной блокиров-
блокировкой ядра, может вызвать функцию schedule о, тем самым добровольно осво-
освобождая Процессор. Однако фуНКЦИЯ schedule () Проверяет ПОЛе lock_depth
замещаемого процесса и, если его значение равно нулю или положительно,
автоматически освобождает семафор kerneisem (см. разд. "Функция
schedule 0" главы 7). Таким образом, никакой процесс, явно вызвавший функ-
функцию schedule о, не сможет удержать глобальную блокировку ядра при пере-
переключении процессов. Впрочем, функция schedule о вновь захватит ему гло-
глобальную блокировку ядра, когда он снова будет выбран для выполнения.
Совсем иная картина получается, когда процесс, имеющий глобальную бло-
блокировку ядра, вытесняется другим процессом. Вплоть до версии 2.6.10 такая
ситуация была невозможна, поскольку получение спин-блокировки автома-
автоматически влечет за собой отключение вытеснения в ядре. Однако теперешняя
реализация глобальной блокировки ядра основана на семафоре, а его получе-
получение не отключает вытеснение в ядре автоматически. На самом деле, разреше-
разрешение вытеснения в ядре внутри критических областей, защищаемых глобаль-
глобальной блокировкой ядра, было главным мотивом изменения реализации. Это
оказывает положительное влияние на время отклика системы.
Когда процесс, обладающий глобальной блокировкой ядра, вытесняется,
функция schedule о не должна освобождать семафор, потому что процесс,
выполняющий код критической области, не добровольно вызвал переключе-
переключение процессов. Действительно, если глобальную блокировку ядра освобо-
освободить, другой процесс может получить ее и испортить данные, с которыми ра-
работал вытесненный процесс.
Чтобы вытесненный процесс не потерял глобальную блокировку ядра, функ-
функция preemptscheduieirqo временно записывает-1 в поле lockdepth деск-
риптора этого процесса. Прочитав такое значение поля, функция schedule о
приходит к выводу, что вытесняемый процесс не удерживает семафор
kerneisem. В результате функция не освобождает семафор kerneisem, и он
по-прежнему принадлежит вытесненному процессу. Когда процесс снова
выбирается планировщиком, функция preemptscheduieirqo восстанав-
восстанавливает старое значение поля lockdepth и позволяет процессу возобновить
выполнение в критической области, защищенной глобальной блокировкой
ядра.
Семафор чтения/записи для дескриптора памяти
Каждый дескриптор памяти, имеющий тип mmstruct, включает в себя собст-
собственный семафор, хранящийся в поле mmapsem (см. разд. "Дескриптор памя-
памяти" главы 9). Этот семафор защищает дескриптор от конфликтов одновре-
одновременного обращения, которые возможны, потому что дескриптор памяти мо-
может быть совместно использован несколькими облегченными процессами.
Например, предположим, что ядро должно создать или расширить область
памяти для некоего процесса. Оно вызывает функцию dommap (), которая вы-
выделяет новую структуру vmareastruct. При этом текущий процесс может
быть приостановлен, если нет свободной памяти, а другой процесс, совмест-
совместно использующий тот же дескриптор памяти, может выполняться. Без сема-
семафора любая операция второго процесса, затрагивающая дескриптор памяти
(например, исключение "ошибка обращения к странице", возникшее из-за
копирования при записи), может привести к серьезной порче данных.
Семафор реализован как семафор чтения/записи, потому что некоторым
функциям ядра, например, обработчику исключения "ошибка обращения к
странице" (см. главу 9), нужно лишь просматривать дескрипторы памяти.
Семафор для списка slab-кэшей
Список дескрипторов slab-кэшей (см. разд. "Дескриптор кэша" главы 8) за-
защищен семафором cachechainsem, который предоставляет процессам ис-
исключительное право на чтение и модификацию списка.
Конфликт одновременного обращения возможен, когда функция
kmemcachecreate () добавляет в список новый элемент, в то время как функ-
функции kmemcacheshrink () И kmemcachereap () последовательно просматрива-
ют список. Однако эти функции никогда не вызываются при обработке пре-
прерывания, и они не могут быть блокированы при обращении к списку. Этот
семафор играет активную роль как в многопроцессорных системах, так и в
однопроцессорных системах с поддержкой вытеснения в ядре.
Семафор для индексного дескриптора
Как мы увидим в главе 12, Linux хранит информацию о файле, расположен-
расположенном на диске, в объекте памяти, который называется индексным дескрипто-
дескриптором. Соответствующая структура данных включает в себя собственный
семафор в поле isem.
При работе с файловой системой может возникнуть огромное количество
конфликтов одновременного обращения. В самом деле, файл на диске яв-
является ресурсом, доступным всем пользователям. Любой процесс может
(в принципе) прочитать содержимое файла, изменить его имя или местопо-
местоположение, уничтожить или скопировать его и т. д. Предположим, например,
что процесс составляет список файлов в некотором каталоге. Каждая диско-
дисковая операция потенциально может быть блокирующей, поэтому даже в одно-
однопроцессорных системах другие процессы могут изменять содержимое ката-
каталога, пока первый процесс находится в середине своей работы. Другой при-
пример: два разных процесса могут одновременно модифицировать один
каталог. Все эти конфликты предотвращаются путем защиты файла каталога
с помощью семафора.
Когда программа использует два или несколько семафоров, существует опас-
опасность взаимной блокировки, потому что два различных управляющих тракта
могут попасть в ситуацию, где каждый будет ждать, пока другой освободит
семафор. Вообще говоря, в Linux редко возникают проблемы с взаимными
блокировками на семафорах, потому что каждый тракт ядра обычно пытается
получить только один семафор за раз. Однако в некоторых случаях ядро
должно получить две или несколько блокировок. Семафоры индексных деск-
дескрипторов могут оказаться вовлеченными в такой сценарий. Например, это
случается в служебной процедуре системного вызова rename (). В операции
участвуют два разных индексных дескриптора, так что требуется получить
два семафора. Чтобы избежать подобных взаимных блокировок, запросы на
семафор выполняются в строго определенном порядке.
ГЛАВА 6
Хронометраж
Огромное количество операций в компьютере производится вследствие хро-
хронометража, часто выполняемого незаметно для пользователя. Например, если
экран автоматически выключается после того, как вы перестали работать за
консолью, это происходит благодаря таймеру, позволяющему ядру отслежи-
отслеживать, сколько времени прошло с последнего нажатия на клавишу или послед-
последнего движения мыши. Если вы получаете предупреждение от системы с
просьбой удалить ряд неиспользуемых файлов, это результат работы про-
программы, идентифицирующей файлы, к которым долго никто не обращался.
Чтобы выполнить свою задачу, программа должна иметь возможность счи-
считывать у каждого файла отметку времени, показывающую момент последнего
обращения к файлу. Такая отметка должна автоматически ставиться ядром.
Не менее важен тот факт, что хронометраж управляет переключением между
процессами вместе с такими заметными операциями ядра, как проверка тайм-
аутов.
Можно назвать два главных вида хронометража ядра Linux:
□ сопровождение текущего времени и даты так, чтобы они могли быть по-
получены пользовательскими программами при посредстве API-интерфейсов
time (), f time () И gettimeof day () И ИСПОЛЬЗОВаны Самим ЯДрОМ ДЛЯ ПОСТа-
новки отметок времени на файлах и сетевых пакетах;
□ поддержка таймеров— механизмов, способных уведомлять ядро или
пользовательскую программу об истечении определенного интервала вре-
времени.
Измерение времени выполняется несколькими электронными схемами, в ос-
основе которых лежат генераторы постоянной частоты и счетчики. Эта глава
состоит из четырех частей. В первых двух разделах описываются аппаратные
устройства, на которых основан хронометраж, и дается общая картина архи-
архитектуры хронометража в Linux. В следующих разделах перечисляются основ-
основные обязанности ядра, связанные с хронометражом: реализация разделения
времени процессора, обновление системного времени и статистики использо-
использования ресурсов, а также поддержка программных таймеров. В последнем раз-
разделе описаны системные вызовы, имеющие отношение к хронометрирова-
хронометрированию, и соответствующие служебные процедуры.
Микросхемы часов и таймеров
В архитектуре 80x86 ядру приходится явным образом взаимодействовать с
аппаратными часами и таймерами нескольких типов. Микросхемы часов
применяются как для отслеживания текущего времени суток, так и для про-
проведения точных измерений времени. Микросхемы таймеров програм-
программируются ядром так, чтобы они генерировали прерывания с фиксированной,
заранее установленной частотой. Такие периодические прерывания чрезвы-
чрезвычайно важны для реализации программных таймеров, используемых ядром и
прикладными программами. Далее мы кратко рассмотрим микросхемы часов
в IBM-совместимых компьютерах.
Часы реального времени
У всех компьютеров имеются часы, называемые часами реального времени
(или RTC, Real Time Clock), которые работают независимо от процессора и
других устройств.
Часы реального времени продолжают "тикать", даже когда компьютер вы-
выключен, поскольку они питаются от небольшой батарейки. Оперативная па-
память CMOS и часы реального времени интегрированы в один чип (Moto-
(Motorola 146818 или эквивалентный).
Часы реального времени могут генерировать периодические прерывания на
IRQ 8 с частотой в диапазоне от 2 до 8192 Гц. Кроме того, их можно запро-
запрограммировать на активизацию линии IRQ 8, когда их показания дойдут до
определенного времени, т. е. они могут работать как будильник.
Операционная система Linux использует часы реального времени только для
получения даты и времени, однако она позволяет процессам программиро-
программировать часы RTC, воздействуя на файл /dev/rtc (см. главу 13). Ядро обращается к
часам реального времени через порты 0x70 и 0x71. Системный администра-
администратор может считывать и изменять показания часов с помощью системной
Unix-программы clock, которая работает непосредственно с этими двумя пор-
портами.
Счетчик отметок времени
У всех микропроцессоров 80x86 есть входной контакт CLK, который прини-
принимает тактовый сигнал от внешнего генератора. Начиная с серии Pentium,
микропроцессоры 80x86 оборудуются счетчиком, который увеличивается с
каждым таким сигналом. Счетчик доступен при посредстве регистра TSC
(Time Stamp Counter, Счетчик отметок времени), содержимое которого мож-
можно прочитать с помощью ассемблерной инструкции rdtsc. Когда ядро поль-
пользуется этим регистром, ему приходится учитывать частоту тактового сигнала.
Если, например, часы выдают сигнал с частотой 1 ГГц, счетчик отметок вре-
времени увеличивается каждую наносекунду.
Операционная система Linux пользуется регистром TSC для проведения бо-
более точных измерений времени, чем те, что выполняются программируемым
таймером интервалов. Для этого система Linux должна определить частоту
тактового сигнала на этапе инициализации. На практике выходит, что, по-
поскольку эта частота не объявляется при компиляции ядра, один и тот же об-
образ ядра может быть запущен на процессорах, часы которых работают с про-
произвольной частотой.
Задача выяснения фактической частоты процессора решается во время за-
загрузки системы. Функция caiibratetsco вычисляет частоту, подсчитывая
количество тактовых сигналов, поступивших в течение приблизительно пяти
миллисекунд. Эта константа получается в результате соответствующей на-
настройки одного из каналов программируемого таймера интервалов1.
Программируемый таймер интервалов
Кроме часов реального времени и счетчика отметок времени, у IBM-
совместимых компьютеров имеется еще одно устройство хронометрирова-
хронометрирования, называемое программируемым таймером интервалов (PIT, Programm-
Programmable Interval Timer). Роль программируемого таймера интервалов аналогична
роли таймера в микроволновой печи: он сообщает пользователю, что время
приготовления блюда истекло. Правда, вместо звукового сигнала это устрой-
устройство генерирует специальное прерывание, называемое прерыванием по тай-
таймеру, уведомляющее ядро об истечении одного или нескольких интервалов
времени2. Еще одно отличие от таймера в микроволновке состоит в том, что
таймер PIT продолжает выдавать прерывания бесконечно с некоторой фикси-
1 Чтобы избежать потери значащих цифр при целочисленной операции деления, функция возвра-
возвращает продолжительность тика в микросекундах, умноженную на 232.
2 Таймер PIT используется также для управления аудиоусилителем, подключенным к внутреннему
динамику компьютера.
рованной частотой, установленной ядром. У любого IBM-совместимого ком-
компьютера есть хотя бы один программируемый таймер интервалов, который
обычно реализуется чипом CMOS 8254, использующим порты ввода/вывода
0x40—0x43.
Как будет подробно описано далее в этом разделе, Linux программирует PIT-
таймер IBM-совместимых компьютеров так, чтобы прерывания генерирова-
генерировались на линии IRQ 0 с частотой приблизительно 1000 Гц, т. е. каждую милли-
миллисекунду. Этот интервал времени называется тиком, и его продолжительность
в наносекундах хранится в переменной ticknsec. В IBM-совместимых ком-
компьютерах переменная ticknsec инициализируется значением 999 848 не (что
приводит к частоте тактового сигнала приблизительно 1000,15 Гц), однако
значение этой переменной может быть автоматически отрегулировано ядром,
если компьютер синхронизируется с внешними часами (см. разд. "Сис-
"Системный вызов adjtimexO" далее в этой главе). Тики отсчитывают время для
всех операций в системе; в определенном смысле, они аналогичны звукам
метронома во время упражнений музыканта.
Вообще говоря, чем короче тики, тем выше точность таймера, что способст-
способствует более сглаженному воспроизведению мультимедийных файлов и более
скорому отклику при мультиплексировании синхронного ввода/вывода (сис-
(системные вызовы poll о и select о). Однако за все приходится платить: корот-
короткие тики требуют, чтобы процессор больше времени проводил в режиме ядра
и меньше— в режиме пользователя. Это приводит к замедлению работы
пользовательских программ.
Частота прерываний по таймеру зависит от аппаратной архитектуры. У мед-
медленных машин тик равен примерно 10 мс A00 прерываний по таймеру в се-
секунду), а у более быстрых он равен приблизительно 1 мс A000 или 1024 пре-
прерываний в секунду).
В коде Linux есть несколько макросов, возвращающих константы, которые
определяют частоту таймерных прерываний. Они приводятся в следующем
списке:
□ hz — возвращает приблизительное количество прерываний по таймеру в
секунду, т. е. их частоту. В IBM-совместимых компьютерах это значение
равно 10003;
□ clocktickrate — возвращает 1 193 182, т. е. частоту внутреннего генера-
генератора чипа 8254;
3 В более современных ядрах Linux значение параметра HZ можно задавать на этапе компиляции,
выбирая один из следующих вариантов (для версии 2.6.20): 100, 250 (установлен по умолчанию)
300, 1000. —Прим. науч.ред.
□ latch — возвращает отношение clocktickrate к hz, округленное до бли-
ближайшего целого. Это значение используется при программировании PIT-
таймера.
Программируемый таймер интервалов инициализируется функцией setup_
pittimero следующим образом:
spin_lock_irqsave(&i8253_lock, flags);
outb_p@x34,0x43);
udelay(lO);
outb_p(LATCH & Oxff, 0x40);
udelay(lO);
outb
(LATCH » 8, 0x40);
spin_unlock_irqrestore(&i8253_lock, flags);
Функция outb (), написанная на языке С, эквивалентна ассемблерной инст-
инструкции outb. Она копирует первый операнд в порт ввода/вывода, указанный
вторым операндом. Функция outbp () аналогична функции outb () с той раз-
разницей, что она создает паузу, выполняя инструкцию "нет операции" и помо-
помогает аппаратуре не сбиться с толку. Макрос udeiayo вводит еще одну не-
небольшую задержку (см. разд. "Функции задержки" далее в этой главе). Пер-
Первый вызов функции outbp () является командой программируемому таймеру
интервалов генерировать прерывания с новой частотой. Следующие вызовы
функций outbp () и outb () передают устройству новую частоту прерываний.
Шестнадцатибитовая константа latch отправляется на восьмибитовый порт
ввода/вывода 0x40 в виде последовательности из двух байтов. В результате
программируемый таймер интервалов начинает выдавать прерывания с час-
частотой приблизительно 1000 Гц, т. е. каждую миллисекунду.
Локальный таймер процессора
Локальный APIC-контроллер, присутствующий в последних моделях микро-
микропроцессоров 80x86 (см. главу 4), предоставляет еще одно устройство для
хронометрирования —локальный таймер процессора.
Локальный таймер процессора является устройством, аналогичным только
что описанному программируемому таймеру интервалов, и может выдавать
однократные или периодические прерывания. Впрочем, имеются и отличия:
□ таймерный счетчик APIC-контроллера имеет длину 32 бита, а длина счет-
счетчика PIT-таймера равна 16 битам. Следовательно, локальный таймер про-
процессора может быть запрограммирован на выдачу прерываний с очень
низкой частотой (счетчик хранит количество тиков, которые должны
пройти перед выдачей прерывания);
□ локальный таймер APIC отправляет прерывание только своему процессо-
процессору, а PIT-таймер возбуждает глобальное прерывание, которое может быть
обработано любым процессором в системе;
□ таймер APIC работает от тактового сигнала шины (или от сигнала шины
APIC в старых компьютерах). Его можно запрограммировать так, что он
будет уменьшать счетчик таймера каждые 1, 2, 4, 8, 16, 32, 64 или
128 тактовых сигналов шины. В отличие от него таймер PIT, который
пользуется собственными тактовыми сигналами, допускает более гибкое
программирование.
Высокоточный таймер событий
Высокоточный таймер событий (НРЕТ, High Precision Event Timer)
представляет собой новый чип таймера, разработанный совместно компания-
компаниями Intel и Microsoft. Хотя НРЕТ-таймеры еще не получили очень широкого
распространения на компьютерах конечных пользователей, Linux 2.6 уже
поддерживает их, и мы вкратце опишем их характеристики.
Высокоточный таймер событий предоставляет в распоряжение ядра несколь-
несколько аппаратных таймеров. Фактически, чип включает в себя до восьми
32-битовых или 64-битовых счетчиков. Каждый счетчик работает от собст-
собственного тактового сигнала, частота которого равняется как минимум 10 МГц.
Таким образом, счетчик увеличивается, по меньшей мере, один раз каждые
100 не. Любой счетчик ассоциирован максимум с тридцатью двумя таймера-
таймерами, каждый из которых состоит из компаратора и регистра совпадений.
Компаратор представляет собой электронную схему, которая сравнивает зна-
значение в счетчике со значением в регистре совпадений и возбуждает аппарат-
аппаратное прерывание, если совпадение имеет место. Некоторые таймеры могут
быть переключены на генерирование периодического прерывания.
Чип НРЕТ может быть запрограммирован с помощью регистров, отображен-
отображенных в память (совсем как I/O APIC). BIOS устанавливает отображение на
этапе загрузки и сообщает ядру операционной системы начальный адрес ото-
отображения. Регистры НРЕТ позволяют ядру считывать и записывать значения
счетчиков и регистров совпадений, программировать однократные прерыва-
прерывания и включать или выключать периодические прерывания на таймерах, ко-
которые их поддерживают.
Вероятно, на материнских платах следующего поколения таймеры НРЕТ и
8254 PIT будут существовать бок о бок, но в будущем, скорее всего, НРЕТ
полностью заменит PIT.
ACPI-таймер управления питанием
ACPI-таймер управления питанием (РМТ, Power Management Timer) является
еще одним устройством синхронизации, имеющимся почти на всех материн-
материнских платах, поддерживающих ACPI. Его тактовый сигнал обладает фикси-
фиксированной частотой приблизительно в 3,58 МГц. Устройство фактически
представляет собой простой счетчик, увеличиваемый на каждом такте. Чтобы
прочитать текущее значение счетчика, ядро обращается к порту вво-
ввода/вывода, адрес которого определяется системой BIOS на этапе инициализа-
инициализации (см. приложение 1).
ACPI-таймер управления питанием предпочтительнее регистра TSC, если
операционная система или BIOS может динамически понижать частоту или
напряжение процессора с целью экономии заряда батареи. Когда это проис-
происходит, частота регистра TSC меняется, что приводит к искажению времени и
другим неприятным эффектам, а частота ACPI-таймера управления питанием
остается неизменной. С другой стороны, высокая частота счетчика отметок
времени весьма удобна для измерения очень коротких интервалов.
Однако при наличии устройства НРЕТ его следует всегда предпочитать дру-
другим микросхемам, по причине его более богатой архитектуры. Табл. 6.2, при-
приведенная далее в этой главе, иллюстрирует, как Linux применяет доступные
микросхемы хронометрирования.
Теперь, когда мы разобрались с аппаратными таймерами, можно обсудить,
как Linux использует их для выполнения всех системных операций.
Архитектура хронометрирования в Linux
Операционная система Linux должна выполнять ряд операций, связанных
с хронометрированием. В частности, ядро периодически:
□ обновляет значение времени, прошедшего с момента старта системы;
□ обновляет время и дату;
□ для каждого процессора определяет, сколько времени уже выполняется
текущий процесс и вытесняет его, если он исчерпал отведенный отрезок
времени. Выделение отрезков времени (также называемых квантами) об-
обсуждается в главе 7;
□ обновляет статистику использования ресурсов;
□ для каждого программного таймера (см. разд. "Программные таймеры и
функции задержки" далее в этой главе) проверяет, истек ли ассоцииро-
ассоциированный с ним интервал времени.
Архитектура хронометрирования в операционной системе Linux представ-
представляет собой набор структур данных и функций ядра, имеющих отношение к
отслеживанию хода времени. У многопроцессорных машин на базе 80x86
архитектура хронометрирования несколько отличается от той, что имеется у
однопроцессорных компьютеров:
□ в однопроцессорной системе вся деятельность, связанная с хронометра-
жом, управляется прерываниями, возбуждаемыми глобальным таймером
(либо программируемым таймером интервалов, либо высокоточным тай-
таймером событий);
□ в многопроцессорной системе все общие операции (например, работа с
программными таймерами) управляются прерываниями, возбуждаемыми
глобальным таймером, в то время как операции, специфичные для отдель-
отдельных процессоров (например, мониторинг времени выполнения текущего
процесса) управляются прерываниями, возбуждаемыми локальным APIC-
таймером.
К сожалению, граница между этими двумя случаями несколько размыта. На-
Например, в некоторых ранних системах с симметричной многопроцессорной
обработкой (SMP) на базе процессоров Intel 80486 не было локальных кон-
контроллеров APIC. Даже в настоящее время встречаются материнские SMP-
карты, сконструированные с таким количеством ошибок, что пользоваться
прерываниями от локальных таймеров в них абсолютно невозможно. В таких
случаях SMP-ядро фактически имеет дело с однопроцессорной архитектурой
хронометрирования. С другой стороны, в современных однопроцессорных
системах имеется один локальный APIC-контроллер, и однопроцессорное
ядро часто пользуется SMP-архитектурой хронометрирования. Однако для
упрощения изложения материала, мы не станем обсуждать эти гибридные
случаи и ограничимся двумя "чистыми" архитектурами хронометрирования.
Архитектура хронометрирования в Linux зависит также и от наличия счетчи-
счетчика отметок времени (TSC), APIC-таймера управления питанием и высокоточ-
высокоточного таймера событий (НРЕТ). Ядро использует две базовые функции работы
со временем: одну для обновления текущего времени, а другую для подсчета
количества наносекунд, прошедших в текущей секунде. Существуют различ-
различные способы получения последнего значения. Некоторые из них весьма точ-
точны и доступны на процессорах, имеющих счетчик отметок времени или вы-
высокоточный таймер событий. На других процессорах применяется менее точ-
точный способ подсчета.
Структуры данных
в архитектуре хронометрирования
Архитектура хронометрирования в Linux 2.6 включает в себя большое коли-
количество структур. Как обычно, мы опишем самые важные переменные, имея
в виду архитектуру 80x86.
Объект-таймер
Чтобы обращаться с различными таймерами унифицированным образом, яд-
ядро использует так называемый объект-таймер, который является дескрипто-
дескриптором типа timeropts и состоит из имени таймера и четырех стандартных ме-
методов, показанных в табл. 6.1.
Таблица 6.1. Поля структуры timer_opts
Имя поля Описание
name Строка, идентифицирующая источник-таймер
mark_of f set Регистрирует точное время последнего тика; вызывается обработ-
обработчиком прерываний по таймеру
get_of f set Возвращает время, прошедшее с последнего тика
monotonic_clock Возвращает количество наносекунд, прошедших после инициали-
инициализации ядра
delay Ждет заданное количество "циклов"
Самыми важными методами объекта-таймера являются markoffset и
getoffset. Метод markoffset вызывается обработчиком прерываний по
таймеру и записывает в соответствующую структуру точное время, в которое
произошел тик. Пользуясь этим значением, метод getof f set вычисляет вре-
время в миллисекундах, прошедшее с момента последнего прерывания по тай-
таймеру (тика). Благодаря этим двум методам архитектура хронометрирования
Linux достигает "субтиковой" точности, т. е. ядро способно определить теку-
текущее время с точностью, гораздо большей, чем один тик. Эта операция назы-
называется интерполяцией времени.
Переменная curtimer хранит адрес объекта-таймера, соответствующего
"наилучшему" источнику-таймеру в системе. Изначально переменная
curtimer указывает на объект timernone, соответствующий фиктивному ис-
источнику, используемому на этапе инициализации ядра. Во время инициали-
инициализации функция seiecttimer() записывает в переменную curtimer адрес
подходящего объекта-таймера. В табл. 6.2 перечислены в порядке предпочте-
предпочтения самые распространенные объекты-таймеры, которые применяются в ар-
архитектуре 80x86 . Из таблицы видно, что функция seiecttimer () выбирает
высокоточный таймер событий, если он доступен. В противном случае она
выбирает ACPI-таймер управления питанием, если он доступен, или счетчик
отметок времени. В качестве последнего средства функция seiecttimero
выбирает программируемый таймер интервалов, который присутствует все-
всегда. В столбце "Интерполяция времени" указаны источники, используемые
методами markoffset и getoffset объекта-таймера; столбец "Задержка"
представляет источники, используемые методом delay.
Таблица 6.2. Типичные объекты-таймеры в архитектуре 80x86,
в порядке предпочтения
ЧьшчШш» I «—- I SST— | »—-
timerhpet Высокоточный таймер собы- НРЕТ НРЕТ
тий (НРЕТ)
timerjpmtmr ACPI-таймер управления пи- ACPI PMT TSC
танием (ACPI PMT)
timertsc Счетчик отметок времени TSC TSC
(TSC)
timerpit Программируемый таймер PIT "Плотный" цикл
интервалов (PIT)
timer none Стандартный фиктивный ис- (нет) "Плотный" цикл
точник (используется во вре-
время инициализации ядра)
Переменная jiffies
Переменная jiffies является счетчиком, показывающим количество тиков,
прошедших со старта системы. Она увеличивается на единицу, когда возни-
возникает прерывание по таймеру, т. е. при каждом тике. В архитектуре 80x86 пе-
переменная jiffies имеет длину 32 бита и, следовательно, диапазон ее значе-
значений исчерпывается приблизительно за 50 дней, — довольно короткий отрезок
для сервера Linux. Однако ядро корректно обрабатывает переполнение пере-
переменной jiffies благодаря макросам time_after, time_after_eq, time_before И
timebeforeeq. Они возвращают корректное значение, даже когда счетчик
совершил "полный оборот".
Читатель, возможно, предполагает, что переменная jiffies инициализирует-
инициализируется нулем при запуске системы, но на самом деле это не так. Она инициализи-
инициализируется значением 0xfffb6c20, которое соответствует 32-битовому целому со
знаком, -300 000. Следовательно, счетчик переполняется через пять минут
после загрузки системы. Это сделано намеренно, чтобы код ядра, содержа-
содержащий ошибки и не проверяющий переполнение переменной jiffies, проявил
себя очень скоро, уже на этапе разработки, и не попал незамеченным в ядра
стабильных версий.
Однако в редких случаях ядру требуется реальное количество системных ти-
тиков, прошедших с момента загрузки, независимо от переполнения перемен-
переменной jiffies. Поэтому в архитектуре 80x86 переменная jiffies приравнива-
приравнивается компоновщиком к младшим 32 битам 64-битового счетчика jiffies_64.
При тике в одну миллисекунду счетчик jiffies_64 делает полный оборот за
несколько сотен миллионов лет, так что мы можем смело предполагать, что
он никогда не переполнится.
Читатель, возможно, недоумевает, почему переменная jiffies в архитектуре
80x86 сразу не объявлена как 64-битовое целое типа unsigned long long. Де-
Дело в том, что в 32-разрядной архитектуре обращение к 64-битовым перемен-
переменным не может происходить атомарно. Поэтому каждая операция чтения всех
64 битов требует некоторой синхронизации, чтобы счетчик не изменился за
время чтения его двух 32-битовых половинок. В результате 64-битовая опе-
операция чтения выполняется гораздо медленнее 32-битовой.
Функция get_jiffies_64 о считывает и возвращает значение переменной
jiffies_64:
unsigned long long get_jiffies_64(void)
{
unsigned long seq;
unsigned long long ret;
do {
seq = read_seqbegin(&xtime_lock);
ret = jiffies_64;
} while (read_seqretry(&xime_lock, seq));
return ret;
}
64-битовая операция чтения защищена seqlock-блокировкой xtimeiock (см.
главу 5). Функция выполняет чтение переменной jiffies_64, пока не получит
гарантию, что переменная не обновлялась одновременно другим управляю-
управляющим трактом ядра.
С другой стороны, критическая область, увеличивающая значение перемен-
переменной jiffies_64, ДОЛЖНа быть Защищена фуНКЦИЯМИ write_seqlock(&xtime_lock)
и write_sequniock(&xtime_iock). Обратите внимание, что инструкция
++jiffies_64 увеличивает также и 32-битовую переменную jiffies, посколь-
поскольку последняя соответствует младшей половине переменной jiffies_64.
Переменная xtime
Переменная xtime хранит текущее время и дату, она представляет собой
структуру типа timespec, состоящую из двух полей:
□ tvsec— количество секунд, прошедших с полуночи 1 января 1970г.
(UTC— Universal Coordinated Time, Универсальное координированное
время);
□ tvnsec — количество наносекунд, прошедших в рамках текущей секунды
(это значение от 0 до 999 999 999).
Переменная xtime обычно обновляется каждый тик, т. е. примерно 1000 раз в
секунду. Как мы увидим в разд. "Системные вызовы, относящиеся к хроно-
хронометрированию " далее в этой главе, пользовательские программы берут теку-
текущее время и дату из переменной xtime. Ядро тоже часто обращается к ней,
например, при обновлении отметок времени в индексном дескрипторе (см.
главу 1).
Seqlock-блокировка xtimeiock позволяет избежать конфликтов одновремен-
одновременного обращения к переменной xtime. Вспомним, что блокировка xtimeiock
защищает также и переменную jiffies_64. Вообще говоря, эта блокировка
используется для определения нескольких критических областей в архитек-
архитектуре хронометрирования.
Архитектура хронометрирования
в однопроцессорных системах
В однопроцессорной системе все действия по хронометрированию управля-
управляются прерываниями, возбуждаемыми программируемым таймером интерва-
интервалов на линии IRQ 0. Как обычно в Linux, некоторые из этих действий выпол-
выполняются как можно скорее, сразу после возбуждения прерывания, а другие
производятся функциями отложенного выполнения (см. разд. "Динамические
таймеры" далее в этой главе).
Этап инициализации
Во время инициализации ядра вызывается функция timeinito, настраи-
настраивающая архитектуру хронометрирования. Обычно4 она выполняет следую-
следующие действия:
1. Инициализирует переменную xtime. Количество секунд, прошедших с по-
полуночи 1 января 1970 года, считывается с часов реального времени при
помощи функции getcmostime (). Поле tvnsec переменной xtime уста-
устанавливается так, чтобы предстоящее переполнение переменной jiffies
совпало с увеличением поля tvsec, т. е. попало на границу секунды.
2. Инициализирует переменную waiitomonotonic. Эта переменная имеет
тот же тип timespec, что и переменная xtime, и хранит количество секунд и
наносекунд, прибавляемых к полям переменной xtime для получения мо-
4 Функция time_init() выполняется до функции mem_init(), инициализирующей структуры
данных, описывающие память. К сожалению, регистры НРЕТ отображаются в память, и инициали-
инициализация чипа НРЕТ должна проводиться после выполнения функции mem_init (). В Linux 2.6 приня-
принято довольно неуклюжее решение: если ядро поддерживает чип НРЕТ, функция time_init () огра-
ограничивается тем, что помечает функцию hpet_time_init () для запуска. Эта функция выполняется
после mem_init () и производит действия, описанные в этом разделе.
нотонного (постоянно увеличивающегося) значения времени. Дело в том,
что прибавление "високосных" секунд и синхронизация с внешними часа-
часами могут внезапно изменить поля tvsec и tvnsec переменной xtime
так, что они не будут увеличены монотонно. Как мы увидим в
разд. "Системные вызовы для таймеров POSIX" далее в этой главе, ино-
иногда ядру требуется действительно монотонный источник времени.
3. Если ядро поддерживает высокоточный таймер событий НРЕТ, оно вызы-
вызывает функцию hpetenabie () для определения, прозондировало ли встро-
встроенное программное обеспечение ACPI соответствующий чип и отобразило
ли его регистры в пространство адресов памяти. Если проверка дала по-
положительный результат, функция hpetenabie () программирует первый
таймер чипа НРЕТ так, чтобы он возбуждал прерывание IRQ 0 тысячу раз
в секунду. Если же чип НРЕТ недоступен, ядро будет использовать про-
программируемый таймер интервалов; этот чип уже настроен функцией
initiRQO на выдачу 1000 прерываний в секунду, как было описано ранее
в этой главе.
4. Вызывает функцию seiecttimer (), чтобы выбрать наилучший таймер-
источник из доступных в системе, и записывает в переменную curtimer
адрес соответствующего объекта-таймера.
5. Вызывает функцию setup_irq(O,&irqO), чтобы настроить шлюз прерыва-
прерываний, соответствующий IRQ 0, линии, ассоциированной с источником пре-
прерываний от системного таймера (PIT или НРЕТ). Переменная irqO опреде-
определена статически следующим образом:
struct irqaction irqO = { timer_interrupt, SA_INTERRUPT, 0,
"timer", NULL, NULL };
С этого момента функция timerinterrupto будет вызываться на каждом
тике с отключенными прерываниями, потому что в поле status главного
дескриптора линии IRQ 0 установлен флаг sainterrupt.
Обработчик прерываний по таймеру
Функция timerinterrupto является служебной процедурой обработки пре-
прерываний от таймера PIT или НРЕТ. Она выполняет следующие действия:
1. Защищает переменные ядра, имеющие отношение к измерению времени, и
с этой целью вызывает функцию writeseqiock () для seqlock-блокировки
xtime_lock (см. главу 5).
2. Выполняет метод markoffset объекта-таймера. Как было сказано ранее
в этой главе, возможны четыре случая:
• переменная curtimer указывает на объект timerhpet. В этом случае
источником прерываний по таймеру является чип НРЕТ. Метод
markof f set убеждается, что с момента последнего тика не было поте-
потеряно ни одно прерывание по таймеру; если эта маловероятная ситуация
все-таки имела место, метод обновляет переменную jiffies_64 соот-
соответствующим образом. Затем он сохраняет текущее значение периоди-
периодического счетчика НРЕТ;
• переменная curtimer указывает на объект timerpmtmr. В этом случае
источником прерываний по таймеру является чип PIT, но ядро пользу-
пользуется ACPI-таймером управления питанием, чтобы измерять время с бо-
более высокой точностью. Метод markof f set убеждается, что с момента
последнего тика не было потеряно ни одно прерывание по таймеру, и
обновляет переменную jiffies_64 в случае необходимости. Затем он
сохраняет текущее значение счетчика ACPI PMT;
• переменная curtimer указывает на объект timertsc. В этом случае ис-
источником прерываний по таймеру является чип PIT, но ядро пользуется
счетчиком отметок времени, чтобы измерять время с более высокой
точностью. Метод markof f set убеждается, что с момента последнего
тика не было потеряно ни одно прерывание по таймеру, и обновляет
переменную jiffies_64 в случае необходимости. Затем он сохраняет
текущее значение счетчика TSC;
• переменная curtimer указывает на объект timerpit. В этом случае ис-
источником прерываний по таймеру является чип PIT, и больше нет ни-
никакой микросхемы таймера. Метод markof f set ничего не предприни-
предпринимает.
3. Вызывает функцию dotimerinterrupt (), которая выполняет следующие
действия:
• увеличивает на единицу значение jiffies_64. Обратите внимание, что
это можно сделать без опасений, поскольку управляющий тракт ядра
все еще держит seqlock-блокировку xtimeiock для записи;
• вызывает функцию updatetimes (), чтобы обновить системную дату и
время и вычислить нагрузку на систему в данный момент. Эти действия
обсуждаются далее в разд. "Обновление времени и даты" и "Обновле-
"Обновление системной статистики";
• вызывает функцию updateprocesstimes о, чтобы выполнить некото-
рые учетные операции для локального процессора;
• вызывает функцию prof iletick () (см. разд. "Профилирование кода
ядра" далее в этой главе)•;
• если системные часы синхронизируются с внешними часами (ранее
был выдан системный вызов adjtimexo), функция вызывает функцию
setrtcmmss о каждые 660 с A1 минут), чтобы подвести часы реально-
реального времени. Эта функциональная особенность помогает системам, ор-
организованным в сеть, синхронизировать свои часы (см. разд. "Сис-
"Системный вызов adjtimexQ " далее в этой главе).
4. Освобождает seqlock-блокировку xtimeiock, вызывая функцию write_
sequnlockO .
5. Возвращает 1, чтобы отметить факт успешной обработки прерывания (см.
главу 4).
Архитектура хронометрирования
в многопроцессорных системах
В многопроцессорных системах могут существовать два различных источни-
источника прерываний по таймеру. Здесь возможны прерывания, возбуждаемые
программируемым таймером интервалов или высокоточным таймером
событий, и прерывания, возбуждаемые локальными таймерами процессоров.
В Linux 2.6 глобальные таймерные прерывания (возбуждаемые чипом PIT
или НРЕТ) сигнализируют о действиях, не связанных с каким-либо конкрет-
конкретным процессором, например, о работе с программными таймерами или об
обновлении системного времени. Локальные таймерные прерывания, наобо-
наоборот, сигнализируют об операциях со временем, относящихся к локальному
процессору. Сюда входят, например, мониторинг продолжительности выпол-
выполнения процесса и обновление статистики использования ресурсов.
Этап инициализации
Обработчик глобальных прерываний по таймеру инициализируется функцией
timeinit (), уже описанной ранее в этой главе.
Ядро Linux резервирует вектор прерывания 239 (Oxef) для локальных преры-
прерываний по таймеру (см. табл. 4.2). На этапе инициализации ядра функция
apicintrinit () устанавливает для таблицы дескрипторов прерываний шлюз
прерываний, соответствует вектору 239, записывая в таблицу адрес низко-
низкоуровневого обработчика прерываний apic_timer_interrupt(). Кроме того,
каждый APIC-контроллер должен быть извещен о том, как часто ему следует
генерировать локальные прерывания. Функция caiibrate_APic_ciock() вы-
вычисляет, сколько шинных синхросигналов было принято локальным APIC-
контроллером загружающегося процессора в течение одного тика A мс). Это
точное значение используется впоследствии, чтобы запрограммировать ло-
локальные APIC-контроллеры на генерирование одного локального прерывания
по таймеру на каждом тике. Это делает функция setupAPictimer (), вызы-
вызываемая один раз для каждого процессора в системе.
Все локальные таймеры APIC синхронизированы, потому что в основе их ра-
работы лежит общий синхросигнал шины. Это означает, что значение, вычис-
вычисленное функцией caiibrateAPicciock() для загружающегося процессора,
годится также и для других процессоров в системе.
Обработчик глобальных прерываний по таймеру
Многопроцессорная версия обработчика timerinterrupt () отличается от од-
однопроцессорной версии по нескольким пунктам:
□ функция dotimerinterrupt (), вызываемая обработчиком timer_
interrupt (), записывает данные в порт чипа I/O APIC для подтверждения
приема прерывания на IRQ-линии таймера;
□ функция updateprocesstimes о не вызывается, потому что она выполняет
действия, относящиеся к конкретному процессору;
□ функция profiieticko не вызывается, потому что она тоже выполняет
действия, относящиеся к конкретному процессору.
Обработчик локальных прерываний по таймеру
Этот обработчик выполняет действия по хронометрированию, относящиеся к
конкретному процессору, а именно профилирование кода ядра и проверку,
сколько уже времени текущий процесс выполняется на данном процессоре.
Ассемблерная функция apictimerinterrupt() эквивалентна следующему
коду:
apic_timer_interrupt:
pushl $B39-256)
SAVE_ALL
movl %esp, %eax
call smp_apic_timer_interrupt
jmp ret_from_intr
Как видите, этот низкоуровневый обработчик очень похож на другие низко-
низкоуровневые обработчики прерываний, описанные в главе 4. Высокоуровневый
обработчик прерываний, называемый smpapictimerinterrupto, выполняет
следующие действия:
1. Получает логический номер процессора (скажем, п).
2. Увеличивает значение поля apictimerirqs у и-го элемента массива
irqstat (см. разд. "Проверка схем слежения немаскируемых прерываний"
далее в этой главе).
3. Подтверждает прием прерывания на локальном APIC-контроллере.
4. Вызывает функцию irqenter () (см. главу 4).
5. Вызывает функцию smp_local_timer_interrupt ().
6. Вызывает фуНКЦИЮ irq_exit () .
ФуНКЦИЯ smplocaltimerinterrupt () ВЫПОЛНЯеТ ДеЙСТВИЯ ПО ХрОНОМеТрИрО
ванию отдельно для каждого процессора. В частности, она делает следующие
основные шаги:
1. Вызывает фуНКЦИЮ profile_tick ().
2. Вызывает функцию update_process_times(), чтобы узнать, сколько време-
времени выполняется текущий процесс, и обновить некоторые статистические
данные локального процессора.
Системный администратор может изменить частоту выборки профайлера ко-
кода ядра, выполнив запись в файл /proc/proflle. Чтобы это изменение вступило
в силу, ядро изменяет частоту генерирования локальных таймерных преры-
прерываний. Тем не менее функция smplocaltimerinterrupt о продолжает вызы-
вызывать фуНКЦИЮ updateprocesstimes () СТрОГО ОДИН раз За ТИК.
Обновление времени и даты
Пользовательские программы считывают текущие время и дату из перемен-
переменной xtime. Ядро должно периодически обновлять эту переменную, чтобы ее
значение было приемлемо точным.
Функция updatetimes о, вызываемая обработчиком глобальных таймерных
прерываний, обновляет значение переменной xtime следующим образом:
void update_times(void)
{
unsigned long ticks;
ticks = jiffies — wall_jiffies;
if (ticks) {
wall_jiffies += ticks;
update_wall_time(ticks) ;
}
calc_load(ticks);
}
Вспомним из предшествующего описания обработчика прерываний по тай-
таймеру, что к моменту выполнения кода этой функции seqlock-блокировка
xtimeiock уже получена для записи.
Переменная waiijiffies хранит время последнего обновления переменной
xtime. Заметим, что значение waiijiffies может быть меньше, чем jiffies-l,
потому что несколько прерываний от таймера могут быть потеряны, напри-
например, когда прерывания долгое время остаются отключенными. Иными слова-
словами, ядро необязательно обновляет переменную xtime на каждом тике. Однако
тики, конечно же, не теряются, и в долгосрочной перспективе переменная
xtiine содержит точное системное время. Проверка на предмет потерянных
Прерываний ПРОИЗВОДИТСЯ МеТОДОМ mark_offset Объекта cur_timer.
Функция update_wall_time () вызывает функцию update_wall_time_one_tick()
подряд ticks раз. В нормальной ситуации каждый вызов прибавляет
1 000 000 к полю xtime. tvnsec. Если значение поля xtime. tv_nsec становится
больше 999 999 999, функция updatewaiitimeo обновляет также и поле
tvsec переменной xtime. Если был сделан системный вызов adjtimexo (по
причинам, разъясненным далее в этой главе), функция может слегка отрегу-
отрегулировать значение 1 000 000, чтобы немного замедлить или ускорить часы.
Функция caicioado описана в разд. "Отслеживание нагрузки на систему"
далее в этой главе.
Обновление системной статистики
Помимо прочих обязанностей, имеющих отношение к хронометрированию,
ядро должно периодически собирать различные данные:
□ проверки лимитов на ресурсы, установленных на процессоре для выпол-
выполняемых процессов;
□ обновления статистики относительно рабочей нагрузки на локальный про-
процессор;
□ вычисления средней нагрузки на систему;
□ профилирования кода ядра.
Обновление статистики локального процессора
Мы уже говорили, что функция updateprocesstimes о вызывается (либо об-
обработчиком глобальных таймерных прерываний в однопроцессорной систе-
системе, либо обработчиком локальных таймерных прерываний в многопроцес-
многопроцессорной системе) для обновления статистики ядра. Эта функция выполняет
следующие действия:
1. Выясняет, сколько времени работает текущий процесс. В зависимости от
того, выполняется он в режиме пользователя или в режиме ядра, при воз-
возникновении прерывания по таймеру функция вызывает либо функцию
account_user_time (), либо функцию account_system_time (). Каждая ИЗ НИХ
фактически выполняет следующие шаги:
• обновляет у дескриптора текущего процесса либо поле utime (количе-
(количество тиков, проведенных процессом в режиме пользователя), либо по-
поле stime (количество тиков в режиме ядра). Дескриптор процесса имеет
еще два поля, cutime и cstime, для подсчета количества тиков, прове-
проведенных потомками процесса в режиме пользователя и режиме ядра
соответственно. Из соображений эффективности, функция update_
processtimes о не затрагивает эти поля, и они обновляются, только ко-
когда процесс-родитель опрашивает состояние одного из своих потомков
(см. главу 3);
• проверяет, исчерпал ли текущий процесс лимит процессорного време-
времени. Если это так, посылает ему сигналы sigxcpu и sigkill. В главе 3
описано, как этот лимит контролируется с помощью поля signai->
riim[RLiMiT_cpu] .riimcur каждого дескриптора процесса;
• вызывает функции account_it_virt() И accountitprof (), чтобы ПрО-
верить таймеры процесса (см. разд. "Системные вызовы setitimerQ и
alarmQ " далее в этой главе);
• обновляет некоторые статистические данные ядра, хранящиеся в пере-
переменной kstat, имеющейся у каждого процессора.
2. Вызывает функцию raisesoftirqo, чтобы активизировать тасклет
timersoftirq на локальном процессоре (см. разд. "Программные тайме-
таймеры и функции отложенного выполнения" далее в этой главе).
3. Если необходима утилизация какой-то старой версии некоторой структу-
структуры, защищенной с помощью механизма RCU, функция проверяет, перешел
ли процессор в состояние покоя, и вызывает функцию taskietscheduleO,
чтобы активизировать тасклет rcutaskiet на локальном процессоре (см.
главу 5).
4. Вызывает функцию scheduiertick (), которая уменьшает для текущего
процесса счетчик отрезка времени и проверяет, истек ли квант. Мы под-
подробно обсудим эти операции в главе 7.
Отслеживание нагрузки на систему
Каждое ядро Unix следит за уровнем активности процессора в системе. Эта
статистика нужна различным администраторным утилитам, таким как top.
Если пользователь введет команду uptime, он получит статистику в виде
"средней загруженности" за последнюю минуту, последние 5 минут и по-
последние 15 минут. В однопроцессорной системе значение 0 говорит об отсут-
ствии активных процессов (кроме процесса swapper с идентификатором 0),
значение 1 свидетельствует о стопроцентной занятости процессора одним
процессом, а значения, превышающие 1, указывают на то, что процессор со-
совместно используется несколькими активными процессами5.
На каждом тике функция updatetimes () вызывает функцию caicioad (), ко-
которая подсчитывает количество процессов, находящихся в состоянии
task_running или task_uninterruptible и использует результат для обновле-
ния средней нагрузки на систему.
Профилирование кода ядра
Linux включает в себя профайлер кода readprofile, используемый разработчи-
разработчиками этой операционной системы для определения, на какие функции уходит
время в режиме ядра. Профайлер идентифицирует горячие зоны ядра, т. е.
наиболее часто выполняемые фрагменты кода ядра. Идентификация таких
фрагментов очень важна, поскольку она позволяет выявить функции ядра,
подлежащие дальнейшей оптимизации.
Профайлер основан на простом алгоритме Монте-Карло. При каждом преры-
прерывании по таймеру ядро определяет, произошло ли данное прерывание в ре-
режиме ядра. Если это так, ядро извлекает из стека значение, которое было в
регистре eip перед прерыванием, и использует его для определения, чем ядро
занималось до прерывания. В долгосрочной перспективе выборки аккумули-
аккумулируются в горячих зонах.
Функция profiieticko собирает данные для профайлера кода. Она вызыва-
вызывается либо функцией dotimerinterrupt () (которую вызывает обработчик
глобальных прерываний по таймеру) в однопроцессорной системе, либо
функцией smpiocaitimerinterrupt () (которую вызывает обработчик ло-
локальных прерываний по таймеру) в многопроцессорной системе.
Чтобы профайлер работал, ядру Linux при загрузке должна быть передана в
качестве параметра строка prof iie=N, где значение n таково, что 2N определя-
определяет размер фрагментов кода, подлежащих профилированию. Собранные дан-
данные можно прочитать в файле /proc/profile. При записи в этот файл счетчики
сбрасываются, а в многопроцессорных системах запись в файл может также
изменить частоту выборки. Впрочем, разработчики ядра, как правило, не об-
обращаются к файлу /proc/profile непосредственно; вместо этого они пользуют-
пользуются Системной командой readprofile.
5 При определении средней загруженности операционная система Linux учитывает все процессы,
находящиеся в состоянии TASK_RUNNING или TASK_UNINTERRUPTIBLE. Однако в нормальных ус-
условиях процессов в состоянии TASK_UNINTERRUPTIBLE мало, и высокая загруженность обычно
означает, что процессор занят.
Ядро Linux 2.6 включает в себя еще один профайлер, называемый oprofile.
Будучи более гибким и настраиваемым, чем readprofile, профайлер oprofile
может применяться для обнаружения горячих зон в коде ядра, приложениях
режима пользователя и системных библиотеках. Когда работает oprofile,
функция prof iletick () вызывает функцию timer notify () для сбора данных,
нужных этому новейшему профайлеру.
Проверка сторожей NMI Watchdog
В многопроцессорных системах операционная система Linux предлагает раз-
разработчикам ядра еще одну функциональную возможность. Это сторожа, ко-
которые могут оказаться полезными для обнаружения ошибок в ядре, вызы-
вызывающих зависание системы. Для активизации такого слежения следует загру-
загрузить ЯДрО С параметром nmi_watchdog.
Работа сторожа основана на одной нетривиальной аппаратной особенности
локальных APIC-контроллеров и контроллеров I/O APIC: они могут генери-
генерировать периодические немаскируемые прерывания на каждом процессоре.
Поскольку немаскируемые прерывания не маскируются ассемблерной инст-
инструкцией cli, сторож может распознать взаимные блокировки, даже когда
прерывания отключены.
В результате этого на каждом тике все процессоры, независимо от того, чем
они до этого занимались, запускают обработчики немаскируемых прерыва-
прерываний. В свою очередь, каждый обработчик вызывает функцию donmi (). Эта
функция получает логический номер процессора п и проверяет поле
apictimerirqs элемента п массива irqstat (см. табл. 4.8). Если процессор
работает правильно, это значение должно отличаться от значения, прочитан-
прочитанного при предыдущем немаскируемом прерывании. Когда с процессором все
в порядке, элемент п поля apictimerirqs увеличивается обработчиком ло-
локальных таймерных прерываний. Если же счетчик не увеличен, значит, обра-
обработчик локальных таймерных прерываний не вызывался целый тик. А в этом
нет ничего хорошего.
Когда обработчик немаскируемого прерывания обнаруживает зависание про-
процессора, он звонит во все колокола: записывает устрашающие сообщения в
системные журналы, выполняет дамп содержимого регистров процессора и
содержимого стека (дамп "kernel oops") и, наконец, уничтожает текущий про-
процесс. Это дает разработчикам ядра возможность понять, в чем проблема.
Программные таймеры
и функции задержки
Таймер — это элемент программного обеспечения, который позволяет функ-
функциям стартовать в определенный момент в будущем, после истечения задан-
ного интервала времени. Термин тайм-аут обозначает момент, в который
истекает интервал времени, ассоциированный с таймером.
Таймеры широко применяются как ядром, так и процессами. Большинство
драйверов устройств пользуется таймерами, чтобы распознать аномальные
ситуации. Например, драйверы гибких дисков используют таймеры для от-
отключения мотора устройства, если к дискете какое-то время нет обращений, а
драйверы параллельных принтеров с помощью таймеров обнаруживают сбои
в работе принтера.
Таймеры нередко используются и программистами для форсирования выпол-
выполнения функций в определенный момент в будущем.
Реализовать таймер относительно легко. Каждый таймер имеет поле, показы-
показывающее, когда таймер должен сработать. Значение этого поля рассчитывается
прибавлением соответствующего количества тиков к текущему значению пе-
переменной jiffies. Затем значение остается неизмененным. Всякий раз, когда
ядро проверяет таймер, оно сравнивает поле срабатывания со значением
jiffies в данный момент. Таймер срабатывает, когда переменная jiffies
становится больше или равной сохраненному значению.
В Linux существует два типа таймеров: динамические таймеры и таймеры
интервалов. Первые используются ядром, а таймеры интервалов могут быть
созданы процессами в режиме пользователя.
Одно небольшое предостережение относительно таймеров в Linux: поскольку
проверка таймеров всегда производится функциями отложенного выполне-
выполнения, которые могут быть запущены через длительный промежуток времени
после того, как были активированы, ядро не может гарантировать, что функ-
функции, запускаемые по таймеру, начнут работать сразу, как только время тай-
таймера истечет. Оно может лишь гарантировать, что они будут выполнены либо
в указанное время, либо с задержкой до нескольких сотен миллисекунд. По
этой причине таймеры не подходят для приложений реального времени, в
которых время истечения интервала должно строго соблюдаться.
Кроме программных таймеров ядро пользуется также функциями задержки,
которые выполняют "плотный" цикл, пока не истечет заданный интервал
времени. Мы обсудим их далее, в разд. "Функции задержки".
Динамические таймеры
Динамические таймеры создаются и уничтожаются динамически. Количество
динамических таймеров, активных в данный момент, ничем не ограничено.
Динамический таймер хранится в следующей структуре типа timeriist:
struct timer_list {
struct list_head entry;
unsigned long expires;
spinlock_t lock;
unsigned long magic;
void (*function)(unsigned long);
unsigned long data;
tvec_base_t *base;
};
Поле function содержит адрес функции, которую надо выполнить, когда вре-
время таймера истечет. Поле data содержит параметр, передаваемый этой функ-
функции. Благодаря полю data существует возможность определить одну функ-
функцию общего назначения, которая будет реагировать на тайм-ауты нескольких
драйверов устройств. Поле data будет хранить идентификатор устройства
или иную информацию, с помощью которой функция сможет отличить уст-
устройство.
Поле expires определяет момент истечения времени таймера. Время в нем
выражено количеством тиков, прошедших с момента запуска системы. Все
таймеры, у которых значение expires меньше или равно значению jiffies,
считаются отработавшими или истекшими.
Поле entry используется для занесения программного таймера в один из дву-
двунаправленных циклических списков, в которых таймеры сгруппированы по
значению их полей expires. Алгоритм, работающий с этими списками, опи-
описан далее в этой главе.
Чтобы создать и активизировать программный таймер, ядро должно:
1. Создать, если необходимо, новый объект типа timeriist, например,^
Это можно сделать несколькими способами:
• создав статическую глобальную переменную в коде;
• определив локальную переменную внутри функции; в этом случае объ-
объект будет храниться в стеке режима ядра;
• включив объект в состав динамически выделяемого дескриптора.
2. Инициализировать объект с помощью функции init_timer(&t). Она запи-
запишет в поле t.base значение null, а спин-блокировку t.iock пометит как
открытую.
3. Записать в поле function адрес функции, которая должна быть активизи-
активизирована, когда время таймера истечет. Если потребуется записать в поле
data значение параметра, передаваемого этой функции.
4. Если динамический таймер еще не занесен в список, присвоить соответст-
соответствующее значение полю expires и вызвать функцию add_timer(&t), которая
поставит элемент t в соответствующий список.
5. В противном случае, если динамический таймер уже находится в списке,
обновить поле expires, вызвав функцию modtimer (), которая заодно поза-
позаботится и о переносе объекта в подходящий список (обсуждается далее).
После того как время таймера истечет, ядро автоматически удалит элемент t
из его списка. Впрочем, иногда процесс должен явным образом удалить тай-
таймер ИЗ СПИСКа С ПОМОЩЬЮ функции del_timer(), deltimersync () ИЛИ
dei_singieshot_timer_sync(). Действительно, спящий процесс может быть
разбужен до истечения таймера, и в этом случае процесс может принять ре-
решение об уничтожении таймера. Вызов функции deitimero для таймера,
уже удаленного из списка, не причинит никакого вреда, поэтому операция
удаления таймера в коде таймерной функции считается хорошей практикой.
В Linux 2.6 динамический таймер привязан к процессору, который его акти-
активизировал, т. е. таймерная функция всегда будет выполняться на том процес-
процессоре, который первым вызвал функцию addtimer () или впоследствии вызвал
функцию modtimerO. Однако функция deitimero и ее аналоги могут деак-
тивизировать любой динамический таймер, даже если он не привязан к ло-
локальному процессору.
Динамические таймеры
и проблема одновременного обращения
Будучи асинхронно активизируемыми, динамические таймеры подвержены
проблемам, связанным с одновременным обращением к ресурсам. Например,
рассмотрим динамический таймер, функция которого работает с непостоян-
непостоянным ресурсом (таким как модуль ядра или файловая структура). Освобожде-
Освобождение ресурса без предварительной остановки таймера может привести к порче
данных, если таймерная функция активизируется, когда ресурс уже не суще-
существует. Таким образом, "золотое правило" рекомендует останавливать таймер
до освобождения ресурса:
del_tinier (&t) ;
X_Release_Resources();
Однако в многопроцессорных системах этот код небезопасен, поскольку не
исключено, что таймерная функция уже выполняется на другом процессоре,
когда вызывается функция deitimer (). В результате ресурсы будут освобо-
освобождены, в то время как таймерная функция еще работает с ними. Чтобы избе-
избежать такой ситуации, ядро предлагает вниманию разработчика функцию
deltimersync о. Она удаляет таймер из списка, а затем проверяет, выполня-
выполняется ли таймерная функция на каком-то другом процессоре. Если это так,
функция deltimersync о ждет завершения таймерной функции.
Функция deitimersync () довольно сложна и медлительна, поскольку она
должна аккуратно обрабатывать случай, когда таймерная функция заново ак-
активизирует сама себя. Если разработчик ядра уверен, что таймерная функция
не запускает таймер снова, он может воспользоваться более простой и быст-
быстрой функцией deisingieshottimersynco, когда требуется деактивизиро-
вать таймер и подождать завершения таймерной функции.
Конечно, существуют и другие проблемы одновременного обращения. На-
Например, корректный способ модификации поля expires у уже активизирован-
активизированного таймера заключается в вызове функции modtimero, а не в удалении
таймера и воссоздании его заново. Во втором случае два управляющих тракта
ядра, стремящиеся модифицировать поле expires у одного таймера, могут
сильно помешать друг другу. Безопасность реализации таймерных функций в
многопроцессорных системах достигается с помощью спин-блокировки lock,
имеющейся в каждом объекте timerlist. Всякий раз, когда ядро должно об-
обратиться к динамическому таймеру, оно отключает прерывания и захватывает
эту спин-блокировку.
Структуры данных для динамических таймеров
Выбрать подходящую структуру данных при реализации динамического тай-
таймера не так-то просто. Объединение всех таймеров в одном списке отрица-
отрицательно скажется на производительности системы, потому что просмотр
длинного списка таймеров на каждом тике будет обходиться слишком доро-
дорого. С другой стороны, сопровождение отсортированного списка не будет бо-
более эффективным решением, т. к. операции вставки и удаления тоже не-
недешевы.
Подход, принятый в Linux, основан на хорошо продуманной структуре, кото-
которая разбивает значения expires на блоки тиков и позволяет динамическим
таймерам успешно проникать из списков с большими значениями expires в
списки с меньшими значениями. Кроме того, в многопроцессорных системах
множество активных динамических таймеров распределено между разными
процессорами.
Основной структурой данных для динамических таймеров является процес-
процессорная переменная (см. главу 5) по имени tvecbases. Она состоит из nrcpus
элементов, по одному на каждый процессор в системе. Каждый элемент явля-
является структурой tvecbaset, которая содержит все данные, необходимые для
работы с динамическими таймерами, привязанными к соответствующему
процессору:
typedef struct tvec_t_base_s {
spinlock_t lock;
unsigned long timer_jiffies;
struct timer_list *running_timer;
tvec_root_t tvl;
tvec_t tv2;
tvec_t tv3;
tvec__t tv4;
tvec__t tv5;
} tvec_base_t;
Поле tvl является структурой типа tvecroott, которая включает в себя мас-
массив vec из 256 элементов listhead, т. е. из списков динамических таймеров.
Это поле содержит все динамические таймеры, время которых истечет в
ближайшие 255 тиков, если таковые имеются.
Поля tv2, tv3 и tv4 являются структурами типа tvect, каждая из которых
включает в себя массив vec из 64 элементов listhead. Эти списки содержат
все динамические таймеры, время которых истечет в ближайшие 214—1, 220—1
и 226-1 тиков соответственно.
Поле tv5 идентично предыдущим, но у него последний элемент массива vec
является списком динамических таймеров с очень большими значениями в
полях expires. Этот список никогда не пополняется таймерами из других
массивов. На рис. 6.1 схематически изображены эти пять групп списков.
Рис. 6.1. Группы списков, ассоциированных с динамическими таймерами
Поле timerjiffies представляет ближайшее время срабатывания динамиче-
динамических таймеров, которое подлежит отслеживанию. Если оно совпадает со зна-
значением jiffies, значит, никакого скопления незавершенных заданий или
функций отложенного выполнения не наблюдается. Если же оно меньше
jiffies, значит, необходимо обработать списки динамических таймеров,
время которых истекло на предыдущих тиков. Это поле инициализируется
значением jiffies при запуске системы и увеличивается только функцией
runtimersoftirqo, описанной в следующем разделе. Обратите внимание,
что поле timerjiffies может очень сильно отстать от значения jiffies, если
функции, обрабатывающие динамические таймеры, не выполнялись долгое
время (например, из-за того, что они были отключены, или было выполнено
много обработчиков прерываний).
В многопроцессорных системах поле runningtimer указывает на структуру
timeriist, содержащую динамические таймеры, с которыми в данный мо-
момент имеет дело локальный процессор.
Работа с динамическим таймером
Несмотря на удачную структуру данных, обработка программных таймеров
требует очень много времени, и ее нельзя поручать обработчику таймерных
прерываний. В Linux 2.6 этот вид деятельности поручен функции отложенно-
отложенного выполнения, а точнее, softirq-функции timersoftirq.
Функция runtimersoftirqo является функцией отложенного выполнения,
ассоциированной с softirq-функцией timersoftirq. Она выполняет следую-
следующие действия:
1. Сохраняет в локальной переменной base адрес структуры tvecbaset,
ассоциированной с локальным процессором.
2. Получает спин-блокировку base->iock и отключает локальные преры-
прерывания.
3. Запускает цикл while, который закончится, когда значение base->
timerjiffies превысит значение jiffies. На каждом шаге цикла функция
выполняет следующие действия:
• вычисляет индекс списка в поле base->tvi, содержащего таймеры, под-
подлежащие обработке в данный момент:
index = base->timer_jiffies & 255;
• если переменная index равна нулю, значит, все списки в поле base->tvi
уже проверены и, следовательно, пусты. Тогда функция организует
"протекание" динамических таймеров, вызывая функцию cascade ():
if (!index &&
(!cascade(base, &base->tv2, (base->timer_jiffies» 8)&63)) &&
(! cascade (base, &base->tv3, (base->timer_jiffies»14) &63) ) &&
(! cascade (base, &base->tv4, (base->timer_jiffies»20) &63) ) )
cascade (base, &base->tv5, (base->timer_jiffies»26) &63) ;
( Примечание )
Рассмотрим первый вызов функции cascade (). Она принимает в качестве ар-
аргументов адрес, хранящийся в переменной base, адрес поля base->tv2 и ин-
индекс списка в поле base->tv2, содержащего таймеры, время которых истечет в
ближайшие 256 тиков. Этот индекс определяется путем анализа соответствую-
соответствующих битов поля base->timer_jiffies. Функция переносит все динамические
таймеры из списка, хранящегося в поле base->tv2, в соответствующие списки
в поле base->tvl, после чего возвращает положительное значение при усло-
условии, что не все списки в поле base->tv2 опустели. Если они пусты, функция
cascade () вызывается еще раз для пополнения поля base->tv2 таймерами из
списка в поле base->tv3 и т. д.
• увеличивает на единицу значение в поле base->timer_jiffies;
• для каждого динамического таймера в списке base->tvi.vec[index]
функция вызывает соответствующую таймерную функцию. В частно-
частности, для каждого элемента t, имеющего тип timeriist, функция вы-
выполняет следующие действия:
п удаляет элемент t из списка, принадлежащего полю base->tvi;
а в многопроцессорных системах записывает в поле base->running_
timer значение &t;
п записывает null в поле t. base;
п освобождает спин-блокировку base->iock и включает локальные
прерывания;
а выполняет таймерную функцию t.function, передавая ей t.data
в качестве аргумента;
D получает спин-блокировку base->iock и отключает локальные пре-
прерывания;
D переходит к следующему таймеру в списке, если таковой имеется;
• все таймеры в списке обработаны. Функция переходит к следующему
шагу самого внешнего цикла while.
4. Самый внешний цикл while закончен, и это означает, что обработаны все
таймеры, время которых истекло. В многопроцессорных системах функ-
функция устанавливает поле base->running_timer в значение null.
5. Освобождает спин-блокировку base->iock и включает локальные преры-
прерывания.
Поскольку значения jiffies и timerjiffies обычно совпадают, самый
внешний цикл while, как правило, выполняется только один раз. Вообще го-
говоря, самый внешний ЦИКЛ выполняется jiffies - base->timer_jiffies + 1
раз. Кроме того, если во время работы функции runtimersof tirq () про-
произойдет прерывание по таймеру, динамические таймеры, время которых ис-
истекает на этом тике, тоже принимаются во внимание, потому что переменная
jiffies асинхронно увеличивается обработчиком глобальных таймерных
прерываний.
Обратите внимание, что функция runtimersof tirq () получает спин-
блокировку base->iock и отключает локальные прерывания непосредственно
перед входом во внешний цикл. Прерывания включаются, и спин-блокировка
освобождается сразу после вызова каждой функции динамического таймера
на все время ее работы. Это гарантирует, что структуры динамического тай-
таймера не будут испорчены чередующимися управляющими трактами ядра.
Резюмируем. Этот довольно сложный алгоритм обеспечивает высочайшую
производительность. Чтобы понять, почему, предположим ради простоты,
что softirq-функция timersoftirq выполняется сразу после возникновения
соответствующего прерывания по таймеру. Тогда в 255 из 256 (в 99.6% слу-
случаев) функция runtimersoftirqo просто выполнит функции таймеров, вре-
время которых истекло, если таковые имеются. Для периодического пополнения
массива base->tvi.vec достаточно 63 раза из 64 разбить один список в поле
base->tv2 на 256 списков в поле base->tvi. Массив base->tv2.vec, в свою оче-
очередь, должен пополняться в 0.006% случаев (то есть каждые 16,4 с). Анало-
Аналогичным образом, массив base->tv3.vec пополняется каждые 17 мин 28 с, а
массив base->tv4.vec— каждые 18 ч 38 мин. Пополнять массив base->
tv5. vec не нужно.
Применение динамических таймеров:
системный вызов nanosleepO
Чтобы продемонстрировать, как на практике ядро применяет результаты ра-
работы всех описанных функций, рассмотрим пример создания и использова-
использования тайм-аута процесса.
Мы обсудим служебную процедуру системного вызова nanosleepO, т.е.
функцию sysnanosieepo, которая принимает в качестве параметра указатель
на структуру timespec и приостанавливает процесс, пока не истечет задан-
заданный интервал времени. Служебная процедура вначале вызывает функцию
copyfromuser(), чтобы скопировать значения, хранящиеся в структуре
timespec режима пользователя, в локальную переменную t. Если структура
timespec определяет ненулевую задержку, функция выполняет следующий
код:
current->state = TASK_INTERRUPTIBLE;
remaining = schedule_timeout(timespec_to_jiffies(&t)+1);
Функция timespectojiffieso преобразует в тики временной интервал,
хранящийся в структуре timespec. На всякий случай, функция
sysnanosieepo прибавляет один тик к значению, вычисленному функцией
timespec_to_jiffies().
Ядро реализует тайм-ауты процессов с помощью динамических таймеров.
Они появляются в функции scheduietimeout (), которая, по сути, выполняет
следующие операторы:
struct timer_list timer;
unsigned long expire = timeout + jiffies;
init_timer (&timer) ;
timer.expires = expire;
timer.data = (unsigned long) currents-
timer, function = process_timeout;
add_timer(&timer);
schedule(); /* process suspended until timer expires */
del_singleshot_timer_sync (&timer) ;
timeout = expire — jiffies;
return (timeout < 0 ? 0 : timeout);
Когда вызывается функция schedule о, для выполнения выбирается другой
процесс. Когда же первый процесс возобновляет свою работу, описываемая
функция удаляет динамический таймер. В последнем операторе функция воз-
возвращает либо 0, если тайм-аут истек, либо количество тиков, оставшихся до
его истечения, если процесс был разбужен по какой-то другой причине.
Когда тайм-аут истекает, выполняется функция таймера:
void process_timeout(unsigned long data)
{
wake_up_process((task_t *) data);
}
Функция processtimeout () принимает в качестве параметра указатель на де-
дескриптор процесса, хранящийся в поле data объекта timer. В результате вы-
выполнение приостановленного процесса возобновляется.
Разбуженный процесс продолжает выполнять системный вызов sysnanosieep ().
Если значение, возвращенное функцией scheduietimeouto, говорит об исте-
истечении тайм-аута процесса (возвращен ноль), системный вызов завершается.
В противном случае системный вызов стартует заново, как разъясняется
в главе 11.
Функции задержки
Программные таймеры бесполезны, когда ядро должно пережидать короткий
отрезок времени, скажем, меньше нескольких миллисекунд. Например, не-
нередко драйверу устройства приходится ждать фиксированное количество
микросекунд, пока аппаратура не завершит некоторую операцию. Поскольку
у динамического таймера велики накладные расходы на настройку, а мини-
минимальное время ожидания сравнительно велико A мс), драйверу устройства
неудобно пользоваться таким таймером.
В подобных ситуациях ядро прибегает к помощи функций udeiayo и
ndeiay (). Первая принимает в качестве параметра временной интервал, вы-
выраженный в микросекундах, и возвращает управление после его истечения;
вторая аналогична ей, но аргумент задает задержку в наносекундах.
Эти две функции определены следующим образом:
void udelay(unsigned long usecs)
{
unsigned long loops;
loops = (usecs*HZ*current_cpu_data.loops_per_jiffy)/1000000;
cur_timer->delay(loops);
}
void ndeiay(unsigned long nsecs)
{
unsigned long loops;
loops = (nsecs*HZ*current_cpu_data.loops_per_jiffy)/1000000000;
cur_timer->delay(loops);
}
Обе используют в своей работе метод delay объекта-таймера curtimer, кото-
который в качестве параметра принимает отрезок времени, выраженный в "цик-
"циклах". Точное значение одного "цикла" зависит от объекта-таймера, на кото-
который ссылается указатель curtimer (см. табл. 6.2):
□ если cur_tiiner указывает на объекты timer_hpet, timer_pmtmr И timer_tsc,
один "цикл" соответствует одному рабочему циклу процессора, т. е. про-
промежутку между двумя соседними тактовыми сигналами процессора;
□ если curtimer указывает на объекты timer_none ИЛИ timerpit, ОДИН
"цикл" соответствует одной итерации "плотного" цикла из программных
инструкций.
На этапе инициализации, после того как указатель curtimer установлен с
ПОМОЩЬЮ фуНКЦИИ selecttimer (), ядро ВЫПОЛНЯеТ функцию calibrate_
delay (), которая определяет, сколько "циклов" помещается в одном тике. По-
лученное значение сохраняется в переменной current_cpu_data.ioops_
perjiffy, чтобы функции udeiayO и ndeiayO могли использовать его для
преобразования микросекунд и, соответственно, наносекунд в "циклы".
Конечно, для точного измерения времени метод cur_timer->deiay () пользует-
пользуется аппаратными устройствами НРЕТ или TSC, если это возможно. В против-
противном случае, если нет ни чипа НРЕТ, ни TSC, метод выполняет loops итераций
"плотного" цикла программных инструкций.
Системные вызовы,
относящиеся к хронометрированию
Несколько системных вызовов позволяют процессам режима пользователя
считывать и устанавливать время и дату, а также создавать таймеры. Мы
приведем их краткий обзор и покажем, как ядро обрабатывает их.
Системные вызовы time() и gettimeofdayO
Процессы в пользовательском режиме могут читать текущее время и дату с
помощью следующих системных вызовов.
□ time () — возвращает количество секунд, прошедших с полуночи 1 января
1970 г. (UTC);
□ gettimeofday () — возвращает структуру timeval, содержащую количество
секунд, прошедших с полуночи 1 января 1970 г. (UTC), и количество мик-
микросекунд, прошедших в текущей секунде (другая структура, timezone, в
настоящее время не используется).
Системный вызов time о постепенно вытесняется системным вызовом
gettimeofdayO, но он сохранен в Linux ради обратной совместимости. Еще
одна широко используемая функция, ftime (), которая теперь больше не явля-
является системным вызовом, возвращает количество секунд, прошедших с полу-
полуночи 1 января 1970 г. (UTC) и количество миллисекунд в текущей секунде.
Системный ВЫЗОВ gettimeofdayO реализуется функцией sysgettimeofdayO.
Чтобы вычислить текущую дату и время, эта функция вызывает функцию
dogettimeof day (), которая выполняет следующие действия:
1. Получает seqlock-блокировку xtimeiock на чтение.
2. Определяет количество микросекунд, прошедших после последнего пре-
прерывания по таймеру, для чего вызывает метод getof f set объекта-таймера
cur_timer:
usec = cur_timer->getoffset();
3. Как было сказано ранее в этой главе, возможны четыре случая:
• если переменная curtimer указывает на объект timerhpet, метод срав-
сравнивает текущее значение счетчика НРЕТ со значением этого счетчика,
сохраненным при последнем вызове обработчика прерываний по тай-
таймеру;
• если переменная curtimer указывает на объект timerpmtmr, метод
сравнивает текущее значение счетчика ACPI PMT со значением этого
счетчика, сохраненным при последнем вызове обработчика прерываний
по таймеру;
• если переменная curtimer указывает на объект timertsc, метод срав-
сравнивает текущее значение счетчика отметок времени со значением этого
счетчика, сохраненным при последнем вызове обработчика прерываний
по таймеру;
• если переменная curtimer указывает на объект timerpit, метод счи-
считывает текущее значение счетчика PIT, чтобы вычислить количество
микросекунд, прошедшее после последнего прерывания, вызванного
программируемым таймером интервалов.
4. Если какое-то таймерное прерывание было потеряно, функция добавляет
к переменной usec соответствующую задержку:
usec += (jiffies - wall_jiffies) * 1000;
5. Прибавляет к значению usec количество микросекунд, прошедших в те-
текущей секунде:
usec += (xtime.tv_nsec / 1000);
6. Копирует содержимое переменной xtime в буфер пространства пользова-
пользователя, определяемый параметром tv данного системного вызова, и прибав-
прибавляет к полю с микросекундами значение переменной usec:
tv->tv_sec = xtime->tv_sec;
tv->tv_usec = xtime->tv_usec + usec;
7. Вызывает функцию read_seqretry() ДЛЯ seqlock-блокирОВКИ xtime_lock И
возвращается к шагу 1, если другой управляющий тракт ядра одновремен-
одновременно захватил блокировку xtimelock на запись.
8. Проверяет поле с микросекундами на предмет переполнения и, в случае
необходимости, корректирует это поле и второе поле:
while (tv->tv_usec >= 1000000) {
tv->tv_usec -= 1000000;
tv->tv_sec++;
}
Процессы в режиме пользователя с привилегиями суперпользователя могут
изменять текущую дату и время с помощью либо морально устаревшего сис-
системного вызова stimeo, либо более нового settimeofdayO. Функция
syssettimeofdayO вызывает функцию dosettimeofday (), КОТОрая ВЫПОЛНЯет
действия, комплементарные к действиям функции dogettimeof day ().
Обратите внимание, что оба системных вызова изменяют значение перемен-
переменной xtime, не затрагивая регистры RTC. Поэтому новое значение времени те-
теряется при выключении системы, если пользователь не выполнит программу
clock для корректировки часов реального времени.
Системный вызов adjtimexQ
Хотя в результате неточности часов все системы рано или поздно отклоняют-
отклоняются от правильного времени, внезапный перевод часов является администра-
административным произволом и довольно рискованным поступком. Представим себе,
что программисты разрабатывают большую программу, и им необходимо,
чтобы устаревший объектный код перекомпилировался в зависимости от от-
отметок времени на файлах. Резкое изменение системного времени может сбить
с толку программу make и привести к некорректной сборке программы. Под-
Поддержание правильных показаний часов важно и при реализации распределен-
распределенной файловой системы в компьютерной сети. В этом случае правильным ре-
решением является подстройка часов на взаимосвязанных компьютерах таким
образом, чтобы отметки времени, ассоциированные с индексными дескрип-
дескрипторами файлов, оставались согласованными. Поэтому системы часто сконфи-
сконфигурированы так, чтобы в них регулярно выполнялся протокол синхрониза-
синхронизации, например, NTP (Network Time Protocol, Сетевой протокол синхрониза-
синхронизации времени), для постепенной корректировки времени на каждом тике.
В операционной системе Linux эта утилита опирается в своей работе на сис-
системный ВЫЗОВ adjtimex ().
Этот системный вызов представлен в Unix в нескольких вариантах, хотя его
не следует использовать в программах, задуманных как переносимые. Он
принимает в качестве параметра указатель на структуру timex, обновляет па-
параметры ядра значениями полей структуры timex и возвращает эту структуру
с текущими значениями параметров ядра. Эти параметры ядра используются
функцией updatewalltimeonetickO ДЛЯ Корректировки количества МИКрО-
секунд, добавляемых к полю xtime. tvusec на каждом тике.
Системные вызовы setitimerQ и alarmQ
Linux позволяет процессам режима пользователя активизировать специаль-
специальные таймеры, называемые таймерами интервалов6. Эти таймеры периодиче-
периодически отправляют процессу сигналы (см. главу 11). Кроме того, есть возмож-
возможность активизировать таймер интервалов так, чтобы он отправил только один
сигнал по истечении заданного времени. Таким образом, каждый таймер ин-
интервалов характеризуется:
□ частотой отправки сигналов или нулевым значением, если генерируется
только один сигнал;
□ временем, остающимся до отправки следующего сигнала.
Предупреждение о точности таймеров, сделанное нами ранее, справедливо и
в отношении таймеров интервалов. Они гарантированно выполняются по
прошествии заданного отрезка времени, но точный момент срабатывания
предсказать невозможно.
Таймеры интервалов активизируются системным вызовом setitimer (), опре-
определенным в стандарте POSIX. Его первый параметр определяет политику,
проводимую таймером:
□ itimerreal— фактический отрезок времени; процесс получает сигналы
SIGALRM.
□ itimervirtual — время, проведенное процессом в режиме пользователя;
процесс получает сигналы sigvtalrm;
□ itimerprof— время, проведенное процессом как в режиме пользователя,
так и в режиме ядра; процесс получает сигналы sigprof.
Таймеры интервалов бывают либо однократно срабатывающими, либо пе-
периодическими. Второй параметр системного вызова setitimer () указывает на
структуру типа itimerval, задающую продолжительность первоначального
интервала таймера (в секундах или наносекундах) и продолжительность ин-
интервала при автоматической повторной активизации таймера (либо ноль, если
таймер однократный). Третий параметр системного вызова setitimer о явля-
является необязательным указателем на некую структуру типа itimerval, которую
системный вызов заполняет предыдущими параметрами таймера.
Чтобы реализовать таймер интервалов для каждой из политик, описанных
ранее, дескриптор процесса включает в себя три пары полей:
□ it_real_incr И it_real_value;
П1 it_virt__incr И it_virt_value;
О it_prof_incr И it_prof_value.
6 Эти программные конструкции не имеют ничего общего с чипом программируемого таймера
интервалов, описанным ранее в этой главе.
Первое поле в каждой паре хранит интервал между двумя сигналами, изме-
измеренный в тиках; второе содержит текущее значение таймера.
Таймер интервалов itimerreal реализуется с помощью динамических тай-
таймеров, потому что ядро должно посылать сигналы процессу, даже когда он не
выполняется на процессоре. Поэтому каждый дескриптор процесса включает
в себя объект "динамический таймер", называемый reaitimer. Системный
вызов setitimer () инициализирует поля объекта reaitimer, а затем вызывает
функцию addtimer (), чтобы занести динамический таймер в соответствую-
соответствующий список. Когда время таймера истечет, ядро выполнит таймерную функ-
функцию itreaifn (). Функция itreaif n () отправляет процессу сигнал sigalrm,
после чего, если значение itreal incr не равно нулю, устанавливает поле
expires, заново активизируя таймер.
Реализация таймеров itimervirtual и itimerprof не требует динамических
таймеров, поскольку эти таймеры интервалов могут быть обновлены по ходу
выполнения процесса. Соответствующие функции accountitvirto и
accountitprof () вызываются функцией update_process_times (), которая
вызывается либо обработчиком прерываний, возбуждаемых программируе-
программируемым таймером интервалов (в однопроцессорной системе), либо обработчика-
обработчиками локальных таймерных прерываний (в многопроцессорной системе). В ре-
результате эти два интервальных таймера обновляются на каждом тике, а когда
их время истечет, то текущему процессу будет отправлен соответствующий
сигнал.
Системный вызов alarm о посылает вызвавшему процессу сигнал sigalrm по
истечении заданного отрезка времени. Он очень похож на системный вызов
setitimer (), сделанный с параметром itimerreal, потому что тоже пользует-
пользуется динамическим таймером reaitimer, включенным в состав дескриптора
процесса. Поэтому вызов alarm о и вызов setitimer о с параметром
itimerreal не могут быть одновременными.
Системные вызовы для таймеров POSIX
Стандарт POSIX 1003.1b вводит новый тип программных таймеров для про-
программ, работающих в режиме пользователя, в частности, для многопоточных
приложений и приложений реального времени. Эти таймеры нередко
называются таймерами POSIX.
Любая реализация таймеров POSIX должна предлагать программам пользо-
пользовательского режима часы POSIX, т. е. виртуальные источники отсчета време-
времени, имеющие заранее определенную точность и другие свойства. Всякий раз,
когда приложению нужно воспользоваться таймером POSIX, оно создает но-
новый таймерный ресурс, выбирая из существующих часов POSIX в качестве
базы для хронометрирования. Системные вызовы, позволяющие пользовате-
пользователям работать с часами и таймерами POSIX, перечислены в табл. 6.3.
Таблица 6.3. Системные вызовы для часов и таймеров POSIX
Системный вызов Описание
clockgettime () Получает текущее значение часов POSIX
clocksettime () Устанавливает текущее значение часов POSIX
clockgetres () Получает точность часов POSIX
timercreate () Создает новый таймер POSIX на базе указанных часов POSIX
timer_gettime () Получает текущее значение и приращение таймера POSIX
timer_settime () Устанавливает текущее значение и приращение таймера POSIX
timergetoverrun () Получает количество "полных оборотов", сделанных таймером
POSIX, время которого истекло
timer_delete () Уничтожает таймер POSIX
clocknanosieepO Переводит процесс в состояние сна, пользуясь часами POSIX
как источником времени
Ядро Linux 2.6 предлагает два вида часов POSIX:
□ clockrealtime— эти виртуальные часы представляют системные часы
реального времени, фактически, переменную xtime. Точность показаний,
возвращаемая системным вызовом clockgetres (), равна 999 848 не, что
соответствует приблизительно 1000 обновлений переменной xtime в се-
секунду;
□ clockmonotonic— эти виртуальные часы представляют системные часы
реального времени, свободные от каких-либо искажений, связанных с
синхронизацией с внешним источником времени. Фактически, эти вирту-
виртуальные часы представлены суммой двух переменных: xtime и waii_
tomonotonic. Точность этих часов POSIX, возвращаемая системным вызо-
вызовом clockgetres (), равна 999 848 наносекундам.
Ядро Linux реализует таймеры POSIX с помощью динамических таймеров.
Таким образом, они аналогичны традиционным таймерам интервалов типа
itimerreal, описанным в предыдущем разделе. Вместе с тем, таймеры
POSIX намного надежнее традиционных таймеров интервалов. Приведем два
существенных различия между ними:
О когда истекает время традиционного таймера интервалов, ядро в любом
случае посылает процессу, активизировавшему таймер, сигнал sigalrm.
Когда же истекает время таймера POSIX, ядро может послать сигнал лю-
бого типа либо всему многопоточному приложению, либо одному задан-
заданному потоку. Ядро также может форсировать выполнение уведомляющей
функции в потоке приложения, а может и ничего не предпринимать (здесь
все зависит от библиотеки режима пользователя, обрабатывающей это со-
событие);
□ если время традиционного таймера интервалов истекало неоднократно, а
процесс режима пользователя не мог принять сигнал (например, потому
что сигнал был блокирован, или процесс не выполнялся), только первый
сигнал доходит до процесса, а остальные сигналы sigalrm теряются. То же
самое справедливо и в отношении таймеров POSIX, но процесс может
сделать системный вызов, чтобы узнать, сколько раз истекало время тай-
таймера с момента генерирования первого сигнала.
ГЛАВА 7
Планирование процессов
Как любая система с разделением времени, Linux создает волшебный эффект
кажущегося одновременного выполнения нескольких процессов, переключа-
переключаясь с одного на другой в течение очень короткого отрезка времени. Собст-
Собственно переключение между процессами обсуждается в главе 3, а настоящая
глава посвящена планированию, т. е. принятию решений, когда переключать-
переключаться и какой процесс выбрать.
Эта глава состоит из трех частей. Разд. "Политика планирования" знакомит
читателя с абстрактными решениями, принимаемыми операционной систе-
системой Linux при планировании работы процессов. В разд. "Алгоритм планиро-
планирования" обсуждаются структуры данных, используемые для реализации
планирования, и соответствующий алгоритм. Наконец, в разд. "Системные
вызовы, относящиеся к планированию" описываются системные вызовы,
имеющие отношение к планированию работы процессов.
Как всегда, для упрощения описания мы ограничиваемся архитектурой
80x86. В частности, мы предполагаем, что система использует модель одно-
однородного доступа к памяти, и что системный тик равен 1 мс.
Политика планирования
Алгоритм планирования в традиционных Unix-подобных операционных сис-
системах должен отвечать нескольким конфликтующим требованиям: быстрое
время отклика процесса, удовлетворительное обеспечение фоновых задач
ресурсами, недопущение "голодной смерти" процессов, сбалансированное
удовлетворение нужд низко- и высокоприоритетных процессов и т. д. Набор
правил, определяющих, когда и как выбирается новый процесс для выполне-
выполнения, называется политикой планирования.
Планирование в Linux основано на технике разделения времени. Несколько
процессов работают "мультиплексно по времени", поскольку время процес-
процессора разбито на отрезки, каждый из которых предоставляется одному рабо-
работающему процессу1. Конечно, один процессор в данный момент времени мо-
может выполнять только один процесс. Если текущий процесс не завершается к
моменту истечения отрезка, или кванта, времени, может произойти переклю-
переключение на другой процесс. Разделение времени основано на прерываниях от
таймера и, таким образом, прозрачно для процессов. Для обеспечения разде-
разделения времени процессора никакой дополнительный код в программах не
требуется.
Политика планирования основана также на классификации процессов в соот-
соответствии с их приоритетом. Для определения текущего приоритета процесса
иногда применяются довольно сложные алгоритмы, но конечный результат
один: с каждым процессом ассоциируется некоторое значение, говорящее
планировщику, насколько уместно позволить процессу выполняться на про-
процессоре.
В Linux приоритет процесса является динамическим понятием. Планировщик
отслеживает, чем занимаются процессы, и периодически корректирует их
приоритеты. Таким образом, процессы, которым долгое время было отказано
в доступе к процессору, "проталкиваются" за счет динамического увеличения
их приоритета. Соответственно, процессы, работающие слишком долго, "на-
"наказываются" понижением приоритета.
Когда речь идет о планировании, процессы традиционно разделяются на при-
привязанные к вводу/выводу и привязанные к процессору. Первые активно рабо-
работают с устройствами ввода/вывода, и у них много времени уходит на ожида-
ожидание завершения операций ввода/вывода. Вторые выполняют вычислительные
операции, требующие много процессорного времени.
Альтернативная классификация различает три вида процессов:
□ Интерактивные процессы — эти процессы постоянно взаимодействуют с
пользователями и, следовательно, много времени проводят в ожидании
нажатия на клавишу или манипуляции с мышью. При поступлении вход-
входных данных процесс должен быть быстро "разбужен", иначе пользователь
сочтет систему медлительной. Как правило, среднее время задержки
должно лежать в интервале от 50 до 150 мс. Разброс между такими за-
задержками тоже должен быть невелик, чтобы пользователь не подумал, что
система ведет себя странно. Типичными интерактивными программами
1 Вспомним, что остановленные или задержанные процессы не могут быть выбраны алгоритмом
планирования для выполнения на процессоре.
являются командные оболочки, текстовые редакторы и графические при-
приложения.
□ Пакетные процессы — эти процессы не нуждаются во взаимодействии с
пользователем и поэтому работают в фоновом режиме. Поскольку от них
не требуется малое время отклика, эти процессы часто получают от пла-
планировщика низкий приоритет. Типичными пакетными программами явля-
являются компиляторы с языков программирования, движки поиска в базах
данных и приложения, выполняющие научные вычисления.
□ Процессы реального времени — эти процессы предъявляют к планирова-
планированию очень жесткие требования. Их ни в коем случае нельзя блокировать
ради процессов с низким приоритетом, а также они должны иметь гаран-
гарантированно малое время отклика с минимальным разбросом. Типичными
программами реального времени являются аудио- и видеоплееры, про-
программы управления роботами и программы, собирающие данные с физи-
физических датчиков.
Предложенные здесь две классификации в определенном смысле независи-
независимы. Например, пакетный процесс может быть привязан либо к вводу/выводу
(сервер базы данных), либо к процессору (программа рендеринга, т. е. визуа-
визуализации изображения). В то время как программы реального времени легко
распознаются как таковые алгоритмом планирования в Linux, простой способ
отличить интерактивную программу от пакетной отсутствует. Планировщик
в Linux 2.6 придерживается сложного эвристического алгоритма, основанно-
основанного на предыдущем поведении процесса, когда решает, следует ли считать
данный процесс интерактивным или пакетным. Естественно, планировщик
"благоволит" интерактивным процессам за счет пакетных.
Программисты могут повлиять на приоритеты планирования с помощью сис-
системных вызовов, приведенных в табл. 7.1. (Подробности можно найти в
разд. "Системные вызовы, относящиеся к планированию" далее в этой главе.)
Таблица 7.1. Системные вызовы, относящиеся к планированию
Системный вызов Описание
nice () Изменить статический приоритет обычного процесса
getpriority () Узнать максимальный статический приоритет группы
обычных процессов
setpriority () Установить статический приоритет группы обычных
процессов
sched_getscheduler () Узнать политику планирования процесса
schedsetscheduler () Установить политику планирования и динамический
приоритет процесса
Таблица 7.1 (окончание)
Системный вызов Описание
schedgetparam () Узнать динамический приоритет процесса
schedsetparam () Установить динамический приоритет процесса
schedyieid () Освободить процессор добровольно, без блокирования
sched_get_priority_min () Узнать минимальное значение динамического
приоритета в данной политике
schedgetprioritymax () Узнать максимальное значение динамического
приоритета в данной политике
schedrrgetinteryal () Узнать значение кванта времени для политики
планирования по кругу
schedsetaf f inity () Установить маску привязки процессора для данного
процессора
schedgetaff inity () Узнать маску привязки процессора для данного
процессора
Вытеснение процессов
Как было сказано в главе 7, процессы в Linux являются вытесняемыми. Когда
процесс переходит в состояние taskrunning, ядро проверяет, превышает ли
его динамический приоритет приоритет текущего процесса. Если это так, вы-
выполнение текущего процесса прерывается, и вызывается планировщик, кото-
который выбирает другой процесс для выполнения (как правило, выбирается про-
процесс, только что ставший выполняемым). Конечно, процесс также может
быть вытеснен, когда истечет его квант времени. Когда это происходит, уста-
устанавливается флаг tif_need_resched в структуре threadinfo текущего процес-
са, а планировщик вызывается по окончании работы обработчика преры-
прерываний.
Рассмотрим в качестве примера сценарий, в котором участвуют только две
программы: текстовый редактор и компилятор. Редактор является интерак-
интерактивной программой и поэтому имеет динамический приоритет выше, чем у
компилятора. Тем не менее он часто приостанавливается, потому что пользо-
пользователь делает паузы на размышление во время ввода данных. Кроме того,
средний интервал времени между двумя нажатиями на клавиши достаточно
велик. Однако как только пользователь нажимает на клавишу, возникает пре-
прерывание, и ядро "будит" процесс текстового редактора. Ядро определяет, что
динамический приоритет редактора выше, чем у процесса current, выпол-
выполняемого в данный момент (компилятора). Поэтому оно устанавливает флаг
tifneedresched у этого процесса, тем самым заставляя планировщик вклю-
читься после того, как ядро закончит обработку прерывания. Планировщик
выбирает редактор и выполняет переключение процессов. В результате вы-
выполнение редактора возобновляется очень быстро, и символ, введенный
пользователем, отображается на экране. Обработав символ, процесс тексто-
текстового редактора сам себя переводит в состояние ожидания следующего нажа-
нажатия на клавишу, и компилятор может возобновить свою работу.
Необходимо отдавать себе отчет, что вытесненный процесс не приостанавли-
приостанавливается, потому что остается в состоянии taskrunning; просто он не использу-
использует процессор. Кроме того, вспомним, что ядро Linux 2.6 является вытесняе-
вытесняемым. Это означает, что процесс может быть вытеснен, когда он выполняется
в режиме ядра или в режиме пользователя. Эта тема была подробно исследо-
исследована в главе 5.
Сколько должен длиться квант времени?
Продолжительность кванта времени исключительно важна для производи-
производительности системы: она не должна быть ни слишком большой, ни слишком
малой.
Если средняя продолжительность кванта очень мала, накладные расходы, вы-
вызванные переключениями процессов, становятся непомерно высокими. На-
Например, предположим, что переключение между процессами занимает 5 мс.
Если квант тоже равен 5 мс, то как минимум половина циклов процессора
уйдет на переключение процессов2.
Если же средняя продолжительность кванта очень велика, исчезнет впечатле-
впечатление, что процессы выполняются параллельно. Например, предположим, что
квант равен пяти секундам. Каждый выполняемый процесс будет работать
около пяти секунд, а затем останавливаться на достаточно долгое время (как
правило, равное пяти секундам, помноженным на количество выполняемых
процессов).
Часто высказывается мнение, что большая продолжительность кванта ухуд-
ухудшает время отклика интерактивных приложений. Обычно это не соответству-
соответствует действительности. Как было показано ранее в разд. "Вытеснение процес-
процессов", интерактивные процессы имеют относительно высокий приоритет, и
они быстро вытесняют пакетные процессы независимо от продолжительно-
продолжительности кванта.
Однако в некоторых случаях слишком длинный квант ухудшает время откли-
отклика системы. Предположим, например, что два пользователя одновременно
2 На практике дела могут обстоять еще хуже. Например, если время, необходимое на переключение
процесса, входит в квант процесса, то все время процессора будет посвящено переключению между
процессами, и ни один из них не продвинется к своему завершению.
вводят две команды в своих оболочках, причем одна команда запускает про-
процесс, привязанный к процессору, а другая — интерактивное приложение. Обе
оболочки ответвляют новые процессы и делегируют им выполнение пользо-
пользовательских команд. Кроме того, предположим, что эти новые процессы име-
имеют одинаковые начальные приоритеты (Linux заранее не знает, является ли
программа пакетной или интерактивной). Теперь, если планировщик выберет
процесс, привязанный к процессору, для выполнения в первую очередь, то
другой процесс, возможно, будет ждать целый квант времени прежде, чем
начнет выполняться. Если квант достаточно велик, пользователю, запус-
запустившему интерактивное приложение, система покажется слишком медли-
медлительной.
Выбор средней продолжительности кванта всегда является компромиссом.
Правило, принятое в Linux, состоит в том, чтобы выбирать максимально
большую продолжительность, стремясь поддерживать удовлетворительное
время отклика.
Алгоритм планирования
Алгоритм планирования, применявшийся в ранних версиях Linux, был до-
довольно прост и прямолинеен. При каждом переключении процессов ядро
просматривало список выполняемых процессов, вычисляло их приоритеты и
выбирало "наилучший" процесс. Основной недостаток такого алгоритма за-
заключается в том, что время, потраченное на выбор процесса, зависит от ко-
количества выполняемых процессов. Алгоритм оказывается слишком дорогим
(то есть расходует слишком много времени) в системах типа high-end, где
выполняются тысячи процессов.
Алгоритм планирования в Linux 2.6 является намного более сложным. Он
спроектирован так, что хорошо масштабируется в соответствии с количест-
количеством выполняемых процессов, поскольку выбирает процесс за постоянное
время, независимо от количества процессов в системе. Алгоритм также хо-
хорошо масштабируется в соответствии с количеством процессоров, поскольку
у каждого процессора имеется собственная очередь выполняемых процессов.
В дополнение к сказанному, новый алгоритм успешнее отличает интерактив-
интерактивные процессы от пакетных. В результате пользователи сильно загруженных
систем ощущают, что интерактивные приложения в Linux 2.6 имеют лучший
отклик, чем в более ранних версиях.
Планировщику всегда удается найти процесс, который следует выполнить.
Всегда существует, по меньшей мере, один выполняемый процесс — процесс
swapper, который имеет идентификатор, равный 0, и выполняется лишь тогда,
когда процессор не может выполнить другие процессы. Как было отмечено в
главе 3, каждый процессор в многопроцессорной системе имеет свой процесс
swapper с идентификатором, равным нулю.
Выполнение любого процесса в Linux всегда планируется в соответствии с
одним из следующих классов планирования:
П schedfifo— процесс, работающий в реальном времени по принципу
"первым вошел — первым вышел". Когда планировщик предоставляет
процессор такому процессу, он оставляет дескриптор процесса на том же
месте в очереди на выполнение. При отсутствии других выполняемых
процессов реального времени с более высоким приоритетом данный про-
процесс продолжает пользоваться процессором, сколько ему нужно, даже ес-
если имеются выполняемые процессы реального времени с тем же приори-
приоритетом;
□ schedrr — процесс, работающий в реальном времени по круговому прин-
принципу. Когда планировщик предоставляет процессор такому процессу, он
переносит дескриптор процесса в конец очереди на выполнение. Эта по-
политика обеспечивает справедливое распределение времени процессора
между всеми процессами реального времени, принадлежащими к классу
schedrr и имеющими одинаковый приоритет;
□ schednormal — обычный процесс, работающий в режиме разделения вре-
времени.
Алгоритм планирования ведет себя по-разному в зависимости от того, явля-
является ли процесс обычным или процессом реального времени.
Планирование обычных процессов
Каждый обычный процесс имеет свой статический приоритет, значение,
используемое планировщиком для определения важности процесса по отно-
отношению к другим обычным процессам в системе. Ядро выражает приоритет
обычного процесса числом в диапазоне от 100 (наивысший приоритет) до 139
(низший приоритет). Обратите внимание, что с увеличением этого значения
приоритет понижается.
Новый процесс всегда наследует статический приоритет родителя. Однако
пользователь может изменить статический приоритет процесса, которым вла-
владеет, передав некоторые "nice-значения" системным вызовам nice о и
setpriority () (см. разд. "Системные вызовы, относящиеся к планированию"
далее в этой главе).
Базовый квант времени
Статический приоритет, по сути дела, определяет базовый квант времени
процесса, т. е. продолжительность кванта времени, присваиваемого процессу,
когда он исчерпывает свой предыдущий квант. Статический приоритет и ба-
базовый квант времени связаны следующей формулой:
Г A40 - статический приоритет)х20,
базовый квант = I если статический приоритет< 120
времени (в мс) ] A40 - статический приоритет)*5,
[ если статический приоритет^ 120
Легко видеть, что чем выше статический приоритет (то есть меньше его чи-
числовое значение), тем дольше длится базовый квант времени. В результате
процессы с более высоким приоритетом обычно получают более продолжи-
продолжительные отрезки времени процессора по сравнению с низкоприоритетными
процессами. В табл. 7.2 приводятся статические приоритеты, базовые кванты
времени и соответствующие nice-значения для обычных процессов, имею-
имеющих тот или иной приоритет. (Таблица также содержит значения дельты ин-
интерактивности и порога времени сна, которые разъясняются далее в этой
главе).
Таблица 7.2. Типичные приоритеты обычных процессов
— |=гКг-|Н| = |^
Наивысший статиче- 100 -20 800 мс -3 299 мс
ский приоритет
Высокий статический 110 -10 600 мс -1 499 мс
приоритет
Статический приори- 120 0 100 мс +2 799 мс
тет по умолчанию
Низкий статический 130 +10 50 мс +4 999 мс
приоритет
Самый низкий стати- 139 +19 5 мс +6 1199 мс
ческий приоритет
Динамический приоритет и среднее время сна
Кроме статического приоритета, у обычного процесса есть еще и динамиче-
динамический. Это значение, лежащее в диапазоне от 100 (наивысший приоритет)
до 139 (низший приоритет). Фактически именно динамический приоритет
нужен планировщику при выборе следующего процесса для выполнения. Он
связан со статическим приоритетом при помощи следующей эмпирической
формулы:
динамический приоритет = ^
= max A00, min (статический приоритет-бонус + 5, 139))
Бонус — это число от 0 до 10. Значения, меньшие пяти, являются, так сказать,
штрафом, понижающим динамический приоритет, а значения, большие пя-
пяти — премия, повышающая динамический приоритет процесса. Бонус, в свою
очередь, зависит от предыдущей истории процесса, а точнее, от среднего
времени сна процесса.
Грубо говоря, среднее время сна — это среднее количество наносекунд, про-
проведенных процессом в ожидании. Следует, однако, заметить, что это не сред-
среднее время работы за истекший интервал. Например, ожидание в состоянии
taskinterruptible вносит вклад в среднее время сна, отличный от того, что
вносит ожидание в состоянии taskuninterruptible. Кроме того, среднее
время сна уменьшается, пока процесс работает. Наконец, среднее время сна
ни в коем случае не может превысить 1 с.
Связь между средним временем сна и бонусными значениями приведена в
табл. 7.3. (Таблица также содержит разбиение отрезков времени; это понятие
обсуждается позже.)
Таблица 7.3. Среднее время сна, бонусные значения и разбиение отрезков времени
Рэзбивниб
Среднее время сна Бонус отрезков времени
Больше или равно 0, но меньше 100 мс 0 5120
Больше или равно 100 мс, но меньше 200 мс 1 2560
Больше или равно 200 мс, но меньше 300 мс 2 1280
Больше или равно 300 мс, но меньше 400 мс 3 640
Больше или равно 400 мс, но меньше 500 мс 4 320
Больше или равно 500 мс, но меньше 600 мс 5 160
Больше или равно 600 мс, но меньше 700 мс 6 80
Больше или равно 700 мс, но меньше 800 мс 7 40
Больше или равно 800 мс, но меньше 900 мс 8 20
Больше или равно 900 мс, но меньше 1000 мс 9 10
1 секунда 10 10
Кроме прочего, среднее время сна используется планировщиком для опреде-
определения, следует ли считать данный процесс интерактивным или пакетным.
Точнее говоря, процесс считается интерактивным, если он удовлетворяет
следующему соотношению:
динамический приоритет< 3 хстатический приоритет/4+28, C)
что эквивалентно
бонус-5 ^ статический приоритет / 4-28.
Выражение "статический приоритет / 4-28" называется дельтой интерактив-
интерактивности. Некоторые типичные значения этого выражения приведены в табл. 7.2.
Следует заметить, что высокоприоритетному процессу гораздо легче попасть
в разряд интерактивных, чем процессу с низким приоритетом. Например,
процесс, имеющий наивысший статический приоритет A00), считается инте-
интерактивным, если значение его бонуса превышает 2, т. е. его среднее время сна
превышает 200 мс. И наоборот, процесс с самым низким приоритетом A39)
никогда не будет считаться интерактивным, потому что его бонусное значе-
значение всегда меньше числа 11, необходимого для достижения дельты интерак-
интерактивности, равной 6. Процесс, имеющий статический приоритет по умолча-
умолчанию A20), становится интерактивным, как только его среднее время сна пре-
превышает 700 мс.
Активные процессы и процессы
с истекшим квантом времени
Даже если обычные процессы, имеющие высокие приоритеты, получают бо-
более длительные кванты времени, они не должны полностью блокировать
процессы с более низкими статическими приоритетами. Чтобы не допустить
"голодной смерти" процессов, процесс, исчерпавший свой квант времени,
замещается низкоприоритетным процессом, чей квант времени еще не истек.
Для реализации этого механизма планировщик поддерживает два непересе-
непересекающихся набора выполняемых процессов:
□ активные процессы — эти выполняемые процессы еще не исчерпали свои
кванты времени, и поэтому им разрешено работать;
□ процессы с истекшим квантом времени — эти выполняемые процессы уже
исчерпали свои кванты времени, и поэтому им запрещено работать, пока у
всех активных процессов не закончатся кванты времени.
Впрочем, общая схема выглядит несколько сложнее, потому что планиров-
планировщик старается повысить производительность интерактивных процессов. Ак-
Активный пакетный процесс, исчерпавший свой квант времени, обязательно
зачисляется в процессы с истекшим квантом. Активный интерактивный про-
процесс, исчерпавший свой квант времени, обычно остается активным. Плани-
Планировщик выделяет ему новый квант и оставляет его в списке активных процес-
процессов. Однако планировщик переносит интерактивный процесс, исчерпавший
свой квант времени, в список процессов с истекшим квантом, если более
"старый" процесс с истекшим квантом уже ждет достаточно долго, или если
какой-нибудь процесс с истекшим квантом имеет более высокий приоритет
(меньшее числовое значение приоритета), чем данный интерактивный про-
процесс. В результате список активных процессов, в конце концов, становится
пустым, и у процессов с истекшими квантами появляется шанс поработать.
Планирование процессов реального времени
Каждый процесс реального времени имеет свой приоритет реального време-
времени, число в диапазоне от 1 (наивысший приоритет) до 99 (самый низкий при-
приоритет). Планировщик всегда предпочитает высокоприоритетные выполняе-
выполняемые процессы низкоприоритетным. Иными словами, процесс реального вре-
времени сдерживает выполнение любого низкоприоритетного процесса все то
время, пока он остается выполняемым. В отличие от обычных процессов
процессы реального времени всегда считаются активными. Пользователь мо-
может изменить приоритет реального времени у процесса при помощи систем-
системных ВЫЗОВОВ sched_setparam () И sched_setscheduler () (см. разд. "Системные
вызовы, относящиеся к планированию1' далее в этой главе).
Если несколько выполняемых процессов реального времени имеют одинако-
одинаковый наивысший приоритет, планировщик выбирает процесс, стоящий первым
в соответствующем списке локального процессора (см. главу 3).
Процесс реального времени замещается другим процессом только при воз-
возникновении одного из следующих событий:
□ процесс вытесняется другим, имеющим более высокий приоритет реаль-
реального времени;
П процесс выполняет блокирующую операцию и переходит в состоя-
состояние ожидания (точнее, в состояние task_interruptible или task_
uninterruptible);
□ процесс останавливается (состояние taskstopped или tasktraced) или
уничтожается (состояние exit_zombie или exitjdead);
П процесс добровольно освобождает процессор с помощью системного вы-
вызова sched_yield() (см. разд. "Системные вызовы, относящиеся к плани-
планированию" далее в этой главе)\
□ процесс работает в реальном времени по круговому принципу (sched rr)
и исчерпал свой квант времени.
Системные вызовы nice о и setpriorityO, сделанные в отношении процесса
реального времени, работающего по круговому принципу, не затрагивают его
приоритет реального времени, а изменяют продолжительность базового кван-
кванта времени. Фактически продолжительность базового кванта времени у про-
цессов реального времени, работающих по круговому принципу, зависит не
от приоритета реального времени, а от статического приоритета, и определя-
определяется формулой A), приведенной ранее в разд. "Планирование обычных про-
процессов".
Структуры данных,
используемые планировщиком
Вспомним из главы 3, что список процессов объединяет все дескрипторы
процессов, а списки очередей на выполнение объединяют все выполняемые
процессы (то есть находящиеся в состоянии taskrunning), кроме процесса
swapper (работающего "на холостом ходу").
Структура runqueue
Структура runqueue является самой важной структурой данных для плани-
планировщика Linux 2.6. Каждый процессор в системе имеет собственную очередь
На ВЫПОЛНеНИе, И ВСе Структуры runqueue ХраНЯТСЯ В Переменной runqueue s,
которая у каждого процессора своя (см. главу 5). Макрос thisrqo возвраща-
возвращает адрес очереди на выполнение у локального процессора, а макрос cpurq(n)
возвращает адрес очереди на выполнение у процессора с индексом п.
В табл. 7.4 перечислены поля структуры runqueue; большинство из них обсу-
обсуждается в последующих разделах этой главы.
Таблица 7.4. Поля структуры runqueue
Тип Имя Описание
spinlock_t lock Спин-блокировка, защищающая списки
процессов
unsigned long nr_running Количество выполняемых процессов
в очередях на выполнение
unsigned long cpuload Коэффициент загрузки процессора,
рассчитанный по среднему количеству
процессов в очереди на выполнение
unsigned long nr_switches Количество переключений между про-
процессами, выполненных процессором
unsigned long nr_uninterruptible Количество процессов, которые рань-
раньше стояли в очередях на выполнение,
а теперь находятся в состоянии
TASK_UNINTERRUPTIBLE (имеет СМЫСЛ
только сумма этих полей по всем оче-
очередям на выполнение)
Таблица 7.4 (продолжение)
Тип Имя Описание
unsigned long expired_timestamp Время занесения самого "старого"
процесса в списки процессов с истек-
истекшим квантом времени
unsigned long long timestamp_last_tick Отметка времени о последнем преры-
прерывании по таймеру
taskt * curr Указатель на дескриптор процесса,
выполняемого в настоящий момент (то
же самое, что current для локального
процессора)
taskt * idle Указатель на дескриптор процесса
swapper для данного процессора
struct mmstruct * prevram Поле, используемое во время пере-
переключения между процессами для хра-
хранения адреса дескриптора памяти
замещаемого процесса
prioarrayt * active Указатель на списки активных
процессов
prioarrayt * expired Указатель на списки процессов
с истекшими квантами времени
prioarrayt [2] arrays Два набора процессов, активных
и с истекшими квантами времени
int best_expired_prio Самый высокий статический приоритет
(минимальное числовое значение)
среди процессов с истекшими кванта-
квантами времени
atomict nr_iowait Количество процессов, которые рань-
раньше стояли в очередях на выполнение,
а теперь ждут завершения дисковых
операций ввода/вывода
struct scheddomain * sd Поле указывает на базовую область
планирования, для данного
процессора
int activebalance Флаг, устанавливаемый, если какой-
либо процесс должен быть перенесен
из этой очереди на выполнение в дру-
другую (для сбалансированности очере-
^цеи)
int pushcpu He используется
task_t * migration_thread Указатель на дескриптор процесса для
потока ядра migration
Таблица 7.4 (окончание)
Тип Имя Описание
struct list_head migration_queue Список процессов, подлежащих уда-
удалению из этой очереди на выполнение
Самыми важными полями структуры runqueue являются те, что имеют отно-
отношение к спискам выполняемых процессов. Каждый выполняемый процесс в
системе принадлежит одной и только одной очереди на выполнение. До тех
пор, пока выполняемый процесс остается в очереди на выполнение, он может
быть выполнен только процессором, которому эта очередь принадлежит. Од-
Однако, как мы увидим чуть позже, выполняемые процессы могут мигрировать
из одной очереди на выполнение в другую.
Поле arrays очереди на выполнение является массивом из двух структур
prioarrayt. Каждая структура представляет набор выполняемых процессов
и содержит 140 голов двунаправленных списков (по одному на каждое воз-
возможное значение приоритета процесса), битовую карту приоритетов и счет-
счетчик процессов, входящих в набор (см. табл. 3.2).
Рис. 7.1. Структура runqueue и два набора выполняемых процессов
Как изображено на рис. 7.1, поле active структуры runqueue указывает на од-
одну из двух структур prioarrayt в массиве arrays, а соответствующий набор
выполняемых процессов включает в себя все активные процессы. Что касает-
касается поля expired, оно указывает на другую структуру в массиве arrays, а соот-
соответствующий набор выполняемых процессов содержит процессы с истекши-
истекшими квантами времени.
Периодически роли двух структур в массиве arrays меняются: активные про-
процессы вдруг становятся процессами с истекшими квантами времени, а про-
процессы с истекшими квантами времени превращаются в активные. Чтобы это
произошло, планировщик просто обменивает содержимое полей active и
expired в очереди на выполнение.
Дескриптор процесса
Каждый дескриптор процесса включает в себя несколько полей, имеющих
отношение к планированию. Они перечислены в табл. 7.5.
Таблица 7.5. Поля дескриптора процесса, имеющие отношение
к работе планировщика
Тип Имя Описание
unsigned long thread_info->f lags Поле хранит флаг TIF_NEED_RESCHED,
который установлен, если должен быть
вызван планировщик
unsigned int thread_info->cpu Логический номер процессора, владею-
владеющий очередью на выполнение, которая
содержит данный выполняемый процесс
unsigned long state Текущее состояние процесса
int prio Динамический приоритет процесса
int static_prio Статический приоритет процесса
struct list_head run_list Указатели на следующий и предыдущий
элементы очереди на выполнение, кото-
которой принадлежит процесс
prio_array_t * array Указатель на набор prio_array_t
очереди на выполнение, включающий
в себя данный процесс
unsigned long sleep_avg Среднее время сна процесса
unsigned long long timestamp Время последнего занесения процесса в
очередь на выполнение или время по-
последнего переключения, в которое был
вовлечен процесс
unsigned long long last_ran Время последнего переключения, в ходе
которого данный процесс был замещен
int activated Код условия, используемый при возоб-
возобновлении работы процесса
unsigned long policy Класс планирования процесса
(SCHEDJTORMAL, SCHED_RR ИЛИ
SCHED_FIFO)
cpumaskt cpusal lowed Битовая маска процессоров, на которых
может выполняться данный процесс
Таблица 7.5 (окончание)
Тип Имя Описание
unsigned int timeslice Количество тиков, оставшихся в кванте
времени процесса
unsigned int f irst_time_slice Флаг, установленный, если процесс ни
разу не исчерпал свой квант времени
unsigned long rt_priority Приоритет реального времени у данного
процесса
Когда создается новый процесс, функция schedforko, вызванная функцией
copyjprocess (), Заполняет ПОЛЯ timeslice у процесса current (родителя) И
процесса р (потомка) следующим образом:
p->time_slice = (current->time_slice +1) » 1;
current->time_slice »= 1;
Иными словами, количество тиков, оставшихся у родителя, делится пополам
между родителем и потомком. Это делается, чтобы не позволить пользовате-
пользователям получить неограниченное время процессора с помощью следующего ме-
метода: родительский процесс создает процесс-потомок, выполняющий тот же
код, а себя уничтожает; при удачно подобранном темпе создания новых про-
процессов потомок всегда будет получать свежий квант времени до истечения
кванта его родителя. Так вот этот программный трюк не работает, потому что
ядро не поощряет ветвление. Аналогичным образом пользователь не сможет
отхватить себе дополнительное процессорное время в ущерб другим пользо-
пользователям, если запустит несколько фоновых процессов из оболочки или откро-
откроет множество окон на графическом рабочем столе. Обобщая, можно сказать,
что процесс не может заполучить ресурсы (если у него нет привилегии назна-
назначить самому себе политику реального времени), создавая многочисленное
потомство.
Если у родителя в кванте остается всего один тик, в результате операции раз-
разделения в поле current->time_siice будет записан ноль, и квант родителя бу-
будет исчерпан. В этом случае функция copyjprocess о записывает 1 в поле
current->time_slice И ВЫЗЫВает функцию scheduler_tick(), КОТОрая ЭТО ПОЛе
уменьшает.
Кроме того, функция copyjprocess о инициализирует некоторые другие поля
дескриптора процесса-потомка, имеющие отношение к планированию:
p->first_time_slice = 1;
p->timestamp = sched_clock();
Здесь устанавливается флаг firsttimesiice, потому что потомок еще ни
разу не исчерпал свой квант времени (если процесс закончится или выполнит
новую программу в течение своего первого отрезка времени, то остаток от-
отрезка времени, принадлежащего потомку, предоставляется родителю). Поле
timestamp инициализируется отметкой времени, возвращенной функцией
schedciock (): в сущности, эта функция возвращает содержимое 64-разряд-
64-разрядного регистра TSC (см. главу 6), преобразованное в наносекунды.
Функции, вызываемые планировщиком
В своей работе планировщик опирается на ряд функций, самыми важными из
которых являются следующие:
□ scheduierticko — поддерживает корректное состояние счетчика time_
slice у текущего процесса;
□ trytowakeup () — будит спящие процессы;
П recaictaskprio () — обновляет динамический приоритет процесса;
□ schedule () — выбирает новый процесс для выполнения;
□ loadbaiance() — поддерживает сбалансированность очередей в много-
многопроцессорной системе.
Функция scheduler_tick()
Мы уже говорили в главе б, что функция scheduierticko вызывается на
каждом тике для выполнения некоторых операций, имеющих отношение к
планированию. Она выполняет следующие действия:
1. Сохраняет в поле timestampiasttick локальной очереди на выполнение
текущее значение регистра TSC, переведенное в наносекунды. Эта отметка
времени возвращаеися функцией schedciocko.
2. Проверяет, является ли текущий процесс процессом swapper на локальном
процессоре. Если является, функция выполняет следующие действия:
• если локальная очередь на выполнение содержит еще один процесс,
кроме процесса swapper, функция устанавливает флаг tifneedresched
текущего процесса, чтобы форсировать перепланирование. Как мы
увидим далее в этой главе, если ядро поддерживает технологию Hyper-
Threading, то логический процессор может работать вхолостую, даже
при наличии выполняемых процессов в его очереди на выполнение, ес-
если приоритеты этих процессов значительно ниже, чем у процесса, ра-
работающего на другом логическом процессоре, ассоциированном с тем
же физическим процессором;
• переходит к шагу 7 (нет необходимости обновлять счетчик отрезка
времени для процесса swapper).
3. Проверяет, указывает ли поле current->array на список активных процес-
процессов в локальной очереди на выполнение. Если это не так, значит, процесс
исчерпал свой квант времени, но еще не был замещен. В таком случае
функция устанавливает флаг tifneedresched текущего процесса, чтобы
форсировать перепланирование, и переходит к шагу 7.
4. Получает СПИН-блОКИрОВКу this_rq () ->lock.
5. Уменьшает счетчик отрезка времени у текущего процесса и проверяет, ис-
истек ли этот квант. Действия функции сильно различаются в зависимости
от класса планирования, к которому принадлежит процесс, и мы вскоре
обсудим их.
6. Освобождает СПИН-блОКИрОВКу thisrq () ->lock.
7. Вызывает функцию rebaiancetick (), которая должна добиться, чтобы
очереди на выполнение у различных процессоров содержали примерно
одинаковое количество выполняемых процессов. Балансировку очередей
мы обсудим в разд. "Балансирование очередей на выполнение в многопро-
многопроцессорных системах" далее в этой главе.
Обновление отрезка времени
у процесса реального времени
Если текущий процесс является процессом реального времени, принадлежа-
принадлежащим классу планирования "первым вошел — первым вышел", функции
scheduierticko ничего не нужно предпринимать. Фактически, в этом случае
текущий процесс не может быть вытеснен процессом с меньшим или равным
приоритетом, и, следовательно, нет смысла поддерживать корректное значе-
значение счетчика отрезка времени.
Если текущий процесс является процессом реального времени, работающим
по круговому принципу, функция scheduierticko уменьшает его счетчик
отрезка времени и проверяет, истек ли этот квант:
if (current->policy == SCHED_RR && !—current->time_slice) {
current->time_slice = task_timeslice(current);
current->first_time_slice = 0;
set_tsk__need_resched(current) ;
list_del(¤t->run_list);
list_add_tail(¤t->run_list,
this_rq()->active->queue+current->prio);
}
Если функция определяет, что квант времени фактически исчерпан, она вы-
выполняет некоторые действия, нацеленные на то, чтобы текущий процесс был
как можно скорее вытеснен, если это необходимо.
Первое из этих действий заключается в "пополнении" счетчика отрезка вре-
времени данного процесса с помощью функции task_timesiice(). Эта функция
рассматривает статический приоритет процесса и возвращает соответствую-
соответствующий базовый квант времени согласно формуле A), приведенной ранее в этой
главе. Кроме того, сбрасывается поле f irsttimesiice процесса current: этот
флаг был установлен функцией copyprocesso в служебной процедуре сис-
системного вызова fork () и должен быть сброшен, как только истечет первый
квант времени процесса.
Затем функция scheduler_tick() ВЫЗЫВает функцию set_tsk_need_resched(),
чтобы установить у процесса флаг tifneedresched. Как было сказано в гла-
главе 4, этот флаг приводит к вызову функции schedule о, с тем, чтобы процесс
current был замещен другим процессом реального времени с тем же (или бо-
более высоким) приоритетом, если таковой имеется.
Заключительное действие функции scheduiertick () состоит в переносе де-
дескриптора процесса на последнее место в списке активных процессов (в оче-
очереди на выполнение) соответственно приоритету процесса current. Помеще-
Помещение текущего процесса в конец очереди гарантирует, что он не будет снова
выбран для выполнения, пока каждый выполняемый процесс реального вре-
времени с тем же приоритетом, что и у current, не получит отрезок процессор-
процессорного времени. В этом весь смысл кругового планирования. Дескриптор пере-
переносится следующим образом: вначале вызывается функция listdeio для
удаления процесса из списка активных процессов в очереди на выполнение, а
затем — функция listaddtaii о для постановки процесса на последнее ме-
место в том же списке.
Обновление отрезка времени у обычного процесса
Если текущий процесс является обычным, функция scheduierticko выпол-
выполняет следующие действия:
1. Уменьшает счетчик отрезка времени (current->time_siice).
2. Проверяет счетчик отрезка времени. Если квант истек, функция выполняет
следующие действия:
• ВЫЗЫВает фуНКЦИЮ dequeue_task() ДЛЯ удаления Процесса current ИЗ
СПИСКа ВЫПОЛНЯеМЫХ Процессов this__rq() ->active;
• ВЫЗЫВает фуНКЦИЮ set_tsk_need_resched(), чтобы установить флаг
tif_need_resched;
• обновляет динамический приоритет процесса current:
current->prio = effective_prio(current);
( Примечание J
Функция effective_prio() читает ПОЛЯ staticprio и sleepavg процесса
current и вычисляет его динамический приоритет по формуле B), приведенной
в разд. "Планирование обычных процессов"ранее в этой главе.
• пополняет квант времени процесса:
current->tiine_slice = task_timeslice(current);
current->first_time_slice = 0;
• еСЛИ ПОЛе expired_timestamp ЛОКаЛЬНОЙ структуры runqueue paBHO нулю
(то есть набор процессов с истекшими квантами времени пуст), функ-
функция записывает в это поле номер текущего тика:
if (! this_rq () ->expired_timestamp)
this_rq()->expired_timestamp = j iffies;
• заносит текущий процесс либо в список активных процессов, либо в
список процессов с истекшими квантами времени:
if (!TASK_INTERACTIVE(current) || EXPIRED_STARVING(this_rq()) {
enqueue_task(current, this_rq()->expired);
if (current->static_prio < this_rq()->best_expired_prio)
this_rq()->best_expired_prio = current->static_prio;
} else
enqueue_task(current, this_rq()->active);
( Примечание )
Макрос taskinteractive возвращает единицу, если процесс распознан как
интерактивный согласно формуле C), приведенной в разд. "Планирование
обычных процессов" ранее в этой главе. Макрос expiredstarving проверяет,
не пришлось ли первому процессу с истекшим квантом ждать в очереди на вы-
выполнение более 1000 тиков, помноженных на количество процессов в очереди
на выполнение плюс один. Если это действительно так, макрос возвращает
единицу. Макрос expiredstarving возвращает единицу и в том случае, если
значение статического приоритета текущего процесса больше статического
приоритета процесса, уже исчерпавшего свой квант времени.
3. В противном случае, если квант времени еще не исчерпан (поле current->
timesiice не равно 0), функция проверяет, не является ли остаток отрезка
времени у текущего процесса слишком длинным:
if (TASK_INTERACTIVE(p) && !((task_timeslice(p) -
p->time_slice) % TIMESLICE_GRANULARITY(p)) &&
(p->time_slice >= TIMESLICE_GRANULARITY(p) ) &&
(p->array == rq->active)) {
list_del(¤t->run_list);
list_add_tail(¤t->run_list,
this_rq()->active->queue+current->prio);
set_tsk_need_resched(p);
}
Макрос timeslicegranularity возвращает произведение количества про-
процессоров в системе и константы, пропорциональной бонусу текущего про-
процесса (см. табл. 7.3). В принципе, квант времени интерактивного процесса
с высоким статическим приоритетом разбивается на несколько кусков
продолжительностью timeslicegranularity каждый, чтобы такой процесс
не монополизировал процессор.
Функция try_to_wakejupO
Функция trytowakeupo будит спящий или остановленный процесс, пере-
переводя его в состояние taskrunning и занося его в очередь на выполнение,
принадлежащую локальному процессору. Например, эта функция вызывает-
вызывается, когда нужно возобновить выполнение процессов в очереди ожидания (см.
главу 3) или процессов, ждущих сигнала (см. главу 11). В качестве параметров
функция принимает:
О указатель на дескриптор (р) процесса, который следует разбудить;
□ маску состояний процесса (state), который можно разбудить;
□ флаг (sync), запрещающий разбуженному процессу вытеснять текущий
процесс, работающий на локальном процессоре.
Функция выполняет следующие действия:
1. Вызывает функцию taskrqiocko, чтобы отключить локальные прерыва-
прерывания и получить блокировку очереди на выполнение rq, принадлежащей
процессору, который последним выполнял процесс (это может быть про-
процессор, отличный от локального). Локальный номер этого процессора хра-
хранится В ПОЛе p->thread_inf o->cpu.
2. Проверяет, принадлежит ли состояние p->state данного процесса маске
состояний state, переданной функции в качестве аргумента. Если это не
так, функция переходит к шагу 7, чтобы завершить свою работу.
3. Если поле р->аггау не содержит null, значит, процесс уже стоит в очереди
на выполнение, и функция переходит к шагу 8.
4. В многопроцессорных системах функция проверяет, должен ли пробуж-
пробуждаемый процесс мигрировать из очереди на выполнение, принадлежащей
процессору, выполнявшему этот процесс последним в очередь какого-
либо другого процессора. Функция выбирает целевую очередь на выпол-
выполнение, руководствуясь некоторыми эвристическими правилами. Напри-
Например:
• если какой-нибудь процессор в системе простаивает, функция выбирает
его очередь на выполнение в качестве целевой. Предпочтение отдается
процессору, ранее выполнявшему процесс, и локальному процессору
именно в этом порядке;
• если рабочая нагрузка процессора, выполнявшего процесс последним,
значительно ниже, чем у локального процессора, функция выбирает
прежнюю очередь на выполнение в качестве целевой;
• если процесс выполнялся недавно, функция выбирает прежнюю оче-
очередь на выполнение в качестве целевой (аппаратный кэш, возможно,
еще содержит данные процесса);
• если перенос процесса на локальный процессор уменьшает дисбаланс
между процессорами, целевой становится локальная очередь на выпол-
выполнение (см. разд. "Балансирование очередей на выполнение в многопро-
многопроцессорных системах" далее в этой главе).
По окончании этого шага функция определила целевой процессор, кото-
который будет выполнять разбуженный процесс, и, соответственно, оче-
очередь rq, в которую следует поставить этот процесс.
5. Если процесс находится в состоянии taskuninterruptible, функция
уменьшает значение в поле nruninterruptibie целевой очереди на выпол-
выполнение и записывает -1 в поле p->activated дескриптора процесса.
6. Вызывает функцию activatetasko, которая, со своей стороны, выполня-
выполняет следующие действия:
• вызывает функцию schedciock () для получения текущей отметки вре-
времени в наносекундах. Если целевым является не локальный процессор,
функция компенсирует сдвиг прерываний от локального таймера с по-
помощью отметок времени, соответствующих последним прерываниям от
таймеров на локальном и целевом процессорах:
now = (sched_clock () — this_rq()->timestamp_last_tick)
+ rq->timestamp_last_tick;
• вызывает функцию recaictaskprio (), передавая ей указатель на деск-
дескриптор процесса и отметку времени, полученную на предыдущем шаге.
Функция recaictaskprio () описана в следующем разделе;
• устанавливает значение поля p->activated (см. табл. 7.6);
• записывает в поле p->timestamp отметку времени, полученную на ша-
шаге 6;
• заносит дескриптор процесса в список активных процессов:
enqueue task(p, rq->active);
rq->nr_running++;
7. Если либо целевой процессор не является локальным, либо флаг sync не
установлен, функция проверяет, не превышает ли динамический приори-
приоритет нового выполняемого процесса приоритет текущего процесса в оче-
очереди rq (p->prio < rq->curr->prio). Если это так, функция вызывает
фуНКЦИЮ resched task(), ЧТОбы ВЫТеСНИТЬ Процесс rq->curr. В ОДНОПрО-
цессорных системах последняя функция просто вызывает функцию
set_tsk_need_resched(), чтобы установить флаг TIF_NEED_RESCHED ДЛЯ
Процесса rq->curr. В МНОГОПрОЦеССОрНЫХ Системах фуНКЦИЯ resched
task (), кроме того, проверяет, было ли равно нулю старое значение флага
tifneedresched, отличается ли целевой процессор от локального и
сброшен ли флаг tifpollingnrflag у процесса rq->curr (то есть целевой
процесс не занимается активным опросом состояния флага tif_need_
resched у данного процесса). Если это условие выполнено, функция
resched_task() вызывает функцию smp_send_reschedule (), чтобы ВОЗбу-
дить межпроцессорное прерывание и форсировать перепланирование
процессов на целевом процессоре (см. главу 4).
8. Записывает в поле p->state данного процесса значение taskrunning.
9. Вызывает функцию taskrquniocko, чтобы разблокировать очередь на
выполнение rq и заново включить локальные прерывания.
10. Возвращает 1 (если процесс успешно разбужен) или 0 (если процесс раз-
разбужен не был).
Функция reca\cjaskjpr\o()
Функция recaictaskprioO обновляет значения среднего времени сна и
динамического приоритета процесса. В качестве параметров она принима-
принимает дескриптор процесса р и отметку времени now, вычисленную функцией
sched__clock ().
Функция recaictaskprio о выполняет следующие действия:
1. Записывает в локальную переменную результат функции:
min (now — p->timestamp, 109 )
Поле p->timestamp содержит отметку момента переключения процесса в
состояние сна; следовательно, в переменной sieeptime хранится количе-
ство наносекунд, проведенных процессом в состоянии сна после послед-
последнего выполнения (или эквивалент одной секунды, если процесс спал
больше).
2. Если значение sieeptime не больше нуля, функция переходит к шагу 8,
чтобы получить обновление среднего времени сна процесса.
3. Проверяет, не является ли процесс потоком ядра, не пробуждается ли он
из состояния task_uninterruptible (поле p->activated равно -1; см. шаг 5
в предыдущем разделе), и не провел ли процесс непрерывно в состоянии
сна время, превышающее заданный порог. Если эти три условия выполне-
выполнены, функция записывает в поле p->sieep_avg эквивалент девятисот тиков
(эмпирическое значение, полученное вычитанием базового кванта време-
времени типичного процесса из максимально возможного среднего времени
сна). Затем функция переходит к шагу 8.
Порог времени сна зависит от статического приоритета процесса. Некото-
Некоторые типичные значения порога приведены в табл. 7.2. Короче говоря, цель
этого эмпирического правила в том, чтобы процессы, долгое время спав-
спавшие в непрерываемом состоянии (в типичном случае — ожидая заверше-
завершение операций дискового ввода/вывода), получили заранее определенное
среднее время сна, достаточно большее, чтобы они были быстро обслуже-
обслужены, но не настолько большое, чтобы другие процессы "умерли голодной
смертью".
4. Выполняет макрос currentbonus для вычисления значения bonus по
предыдущему среднему времени сна процесса (см. табл. 7.3). Если
(ю - bonus) больше нуля, функция умножает sieeptime на это число. По-
Поскольку переменная sieeptime складывается со средним временем сна
процесса (см. шаг 6 ниже), чем меньше текущее среднее время сна, тем
быстрее оно будет расти.
5. Если процесс находится в состоянии taskuninterruptible и не является
потоком ядра, функция выполняет следующие дополнительные действия:
• сравнивает среднее время сна p->sieep_avg и порог времени сна (см.
табл. 7.2). Если среднее время больше либо равно порогу, функция
сбрасывает локальную переменную sieepavg в ноль (тем самым про-
пропуская подстройку среднего времени сна) и переходит к шагу 6;
• если сумма sieepavg + p->sieep_avg больше либо равна порогу време-
времени сна, функция устанавливает поле p->sieep_avg в значение порога, а
переменную sieepavg сбрасывает в ноль.
Ограничивая в определенной степени приращение среднего времени сна
процесса, функция мало поощряет пакетные процессы, спящие слишком
долго.
6. Складывает значение sieeptime со средним временем сна процесса
(p->sleep_avg).
7. Проверяет, превышает ли значение p->sieep_avg 1000 тиков (в наносекун-
наносекундах). Если превышает, усекает это значение до 1000 тиков (в наносекун-
наносекундах).
8. Обновляет динамический приоритет процесса:
p->prio = effective_prio(p);
Функция ef f ectiveprio () уже была описана ранее в этой главе.
Функция scheduled
Функция schedule () реализует планировщик. Ее цель — найти процесс в оче-
очереди на выполнение, а затем выделить ему процессор. Она вызывается либо
напрямую, либо "ленивым" (отложенным) образом, несколькими функциями
ядра.
Прямой вызов
Планировщик вызывается напрямую, если процесс current должен быть не-
немедленно блокирован, в силу недоступности необходимого ему ресурса.
В этом случае процедура ядра, блокирующая процесс, поступает следующим
образом:
1. Ставит процесс current в соответствующую очередь ожидания.
2. Меняет состояние процесса current либо на taskinterruptible, либо на
TASKJJNINTERRUPTIBLE.
3. Вызывает функцию schedule ().
4. Проверяет, доступен ли ресурс. Если недоступен, переходит к шагу 2.
5. Когда ресурс становится доступным, удаляет процесс current из очереди
ожидания.
Процедура ядра многократно проверяет, доступен ли ресурс, необходимый
процессу. Если ресурс недоступен, она предоставляет процессор какому-
нибудь другому процессу, вызывая функцию schedule о. Впоследствии, когда
планировщик снова выделяет процессор этому процессу, доступность ресур-
ресурса проверяется снова. Эти действия аналогичны действиям функции
waitevent () и сходных функций, описанных в главе 3.
Планировщик также вызывается напрямую многими драйверами устройств,
выполняющими продолжительные итерационные задания. При каждой ите-
итерации цикла драйвер проверяет флаг tifneedresched и, в случае необходи-
мости, вызывает функцию schedule о, чтобы добровольно освободить про-
процессор.
Ленивый вызов
Планировщик может быть вызван и "ленивым" образом, для чего устанавли-
устанавливается флаг tifneedresched. Поскольку проверка этого флага всегда проис-
происходит до возобновления работы процесса в режиме пользователя (см. гла-
главу^, функция schedule о несомненно будет вызвана в какой-то момент
в ближайшем будущем.
Типичными примерами "ленивого" вызова планировщика являются следую-
следующие:
□ процесс current до конца использовал свой квант процессорного времени;
ВЫЗОВ делается функцией scheduler_tick ();
□ процесс разбужен, и его приоритет выше, чем у текущего. Вызов делает
функция try_to_wake__up ();
□ сделан системный вызов sched_setscheduier () (см. разд. "Системные вы-
вызовы, относящиеся к планированию" далее в этой главе).
Действия функции scheduleQ
перед переключением процесса
Цель функции schedule о состоит в замене текущего процесса на какой-то
другой. То есть основным результатом ее деятельности является обновление
локальной переменной next так, чтобы она указывала на дескриптор процес-
процесса, выбранного для замены текущего. Если ни один выполняемый процесс в
системе не имеет приоритета, более высокого, чем у текущего процесса, то
процесс next, в конечном счете, совпадет с процессом current, и никакого
переключения не произойдет.
Функция schedule () начинает работу с отключения вытеснения в ядре и ини-
инициализирует несколько локальных переменных:
need_resched:
preempt_disable();
prev = current;
rq = this_rq();
Здесь указатель, находящийся в current, сохраняется в переменной prev, a
адрес очереди на выполнение, принадлежащей локальному процессору, со-
сохраняется в переменной rq.
Затем функция schedule () убеждается, что процесс prev не держит глобаль-
глобальную блокировку ядра (см. главу 5):
if (prev->lock_depth >= 0)
up(&kernel_sem);
Обратите внимание, что функция schedule о не меняет значение поля
lockdepth. Когда процесс prev возобновляет свое выполнение, функция за-
заново захватит мьютекс kerne is em, если значение этого поля будет неотрица-
неотрицательным. Таким образом, глобальная блокировка ядра автоматически осво-
освобождается и обновляется по ходу переключения процессов.
После этого вызывается функция schedciocko для чтения регистра TSC и
преобразования его значения в наносекунды. Полученная отметка времени
сохраняется в локальной переменной now. Затем функция schedule о вычис-
вычисляет продолжительность отрезка процессорного времени, используемого
процессом prev:
now = sched_clock();
run_time = now — pr ev->time stamp ;
if (run_time > 1000000000)
run_time = 1000000000;
Как обычно, происходит усечение до одной секунды (выраженной в наносе-
наносекундах). Переменная runtime нужна, чтобы "выставить счет" процессу за
использование процессора. Однако процессу с большим средним временем
сна делается "поблажка":
run_time /= (CURRENT_BONUS(prev) ? : 1);
Вспомним, что макрос currentbonus возвращает значение между 0 и 10, про-
пропорциональное среднему времени сна процесса.
Прежде чем приступать к просмотру выполняемых процессов, функция
schedule о должна отключить локальные прерывания и получить спин-
блокировку, защищающую очередь на выполнение:
spin_lock_irq(&rq->lock) ;
Как было показано в главе 3, может оказаться, что процесс prev в данный мо-
момент завершает свою работу. Чтобы распознать этот случай, функция
schedule () проверяет флаг pf_dead:
if (prev->flags & PF_DEAD)
prev->state = EXITJDEAD;
Затем функция schedule о изучает состояние процесса prev. Если процесс не
является выполняемым и не был вытеснен в режиме ядра (см. главу 4), он
должен быть удален из очереди на выполнение. Однако, если у него есть не
заблокированные сигналы, ожидающие доставки, и он находится в состоя-
состоянии task_interruptible, функция переводит процесс в состояние task_running
и оставляет его в очереди. Такая операция — не то же самое, что предостав-
предоставление процессора процессу prev. Она просто дает процессу prev шанс быть
выбранным для выполнения:
if (prev->state != TASK_RUNNING &&
!(preempt_count() & PREEMPT_ACTIVE)) {
if (prev->state == TASK_INTERRUPTIBLE && signal_pending(prev))
prev->state = TASK_RUNNING;
else {
if (prev->state == TASKJJNINTERRUPTIBLE)
rq->nr_uninterruptible++;
deactivate_task(prev, rq);
}
}
Функция deactivatetask () удаляет процесс из очереди на выполнение:
rq->nr_running—;
dequeue_task(р, p->array);
p->array = NULL;
Теперь функция schedule о проверяет количество выполняемых процессов,
оставшихся в очереди. Если таковые имеются, функция вызывает функцию
dependentsieeperO. В большинстве случаев вызванная функция немедленно
возвращает ноль. Однако, если ядро поддерживает технологию Hyper-
Threading, функция проверяет приоритет процесса, который сейчас будет вы-
выбран для выполнения. Если этот приоритет значительно ниже, чем у соседне-
соседнего процесса, уже выполняемого на логическом процессоре того же физиче-
физического процессора, то в данном случае функция schedule о отказывается от
выбора низкоприоритетного процесса и выполняет процесс swapper:
if (rq->nr_running) {
if (dependent_sleeper(smp_processor_id(), rq)) {
next = rq->idle;
goto switch_tasks;
}
}
Если в очереди нет выполняемых процессов, происходит вызов функции
idiebaiance(), чтобы перенести несколько выполняемых процессов в ло-
локальную очередь. Функция idiebaiance() аналогична функции ioad_
balance (), описанной далее в этой главе.
if (!rq->nr_running) {
idle_balance(smp_processor_id(), rq);
if (!rq->nr_running) {
next = rq->idle;
rq->expired timestamp = 0;
wake_sleeping_dependent(smp_processor_id(), rq) ;
if (!rq->nr_running)
goto switch_tasks;
}
}
Если функции idiebaiance () не удается перенести какой-нибудь процесс в
локальную очередь на выполнение, функция schedule о вызывает функцию
wakesieepingdependent о, чтобы перепланировать выполняемые процессы в
простаивающих процессорах (то есть в процессорах, выполняющих процесс
swapper). Как было сказано при обсуждении функции dependentsleeperо,
этот нетипичный случай может произойти, если ядро поддерживает Hyper-
Threading. В однопроцессорных системах, или если все попытки перенести
хотя бы один процесс в локальную очередь на выполнение закончились не-
неудачей, функция выбирает процесс swapper на роль процесса next и перехо-
переходит к следующему этапу.
Предположим, функция schedule о определила, что очередь на выполнение
содержит несколько выполняемых процессов. Теперь ей нужно убедиться,
что хотя бы один из них активен. Если это не так, функция обменивает со-
содержимое ПОЛеЙ active И expired В Структуре runqueue. Таким образом, Все
процессы с истекшими квантами времени становятся активными, а опустев-
опустевший набор процессов готов принять процессы, кванты которых истекут в бу-
будущем.
array = rq->active;
if (!array->nr_active) {
rq->active = rq->expired;
rq->expired = array;
array = rq->active;
rq->expired_timestamp = 0;
rq->best_expired_prio = 140;
}
Сейчас самое время поискать выполняемый процесс в структуре prioarrayt
с активными процессами (см. главу 3). Во-первых, функция schedule о ищет
первый ненулевой бит в битовой маске набора активных процессов. Вспом-
Вспомним, что бит в маске установлен, если соответствующий список приоритетов
не пуст. Индекс первого ненулевого бита укажет на список процессов, наибо-
наиболее подходящих для выполнения. Затем из этого списка извлекается первый
дескриптор процесса:
idx = sched_find first bit(array->bitmap);
next = list_entry(array->queue[idx].next, task_t, run_list);
В основе кода функции schedfindfirstbit о лежит ассемблерная инструк-
инструкция bsfi, возвращающая индекс младшего из битов, установленных в едини-
единицу в 32-битовом слове.
Теперь локальная переменная next хранит указатель на дескриптор процесса,
который заменит процесс prev. Функция schedule о читает значение из поля
next->activated. Это поле кодирует состояние процесса на момент пробуж-
пробуждения в соответствии с табл. 7.6.
Таблица 7.6. Значения поля activated в дескрипторе процесса
Значение Описание
0 Процесс находился в состоянии task_running
1 Процесс находился в состоянии task_interruptible или task_stopped, и
его разбудила служебная процедура системного вызова или поток ядра
2 Процесс находился в состоянии task_interruptible или task_stopped, и
его разбудил обработчик прерывания или функция, отложенного выполне-
выполнения
-1 Процесс находился в состоянии taskjjninterruptible на момент пробу-
пробуждения
Если next является обычным процессом, и его пробуждение происходит из
состояния task_interruptible или task_stopped, планировщик добавляет к
среднему времени сна процесса количество наносекунд, прошедших с мо-
момента постановки процесса в очередь на выполнение. Иными словами, время
сна процесса увеличивается и включает в себя время, проведенное им в оче-
очереди на выполнение в ожидании процессора:
if (next->prio >= 100 && next->activated > 0) {
unsigned long long delta = now — next->timestamp;
if (next->activated == 1)
delta = (delta * 38) / 128;
array = next->array;
dequeue_task(next, array);
recalc task prio(next, next->timestamp + delta);
enqueue_task(next, array);
}
next->activated = 0;
Обратите внимание, что планировщик проводит различие между процессом,
разбуженным обработчиком прерывания или функцией отложенного выпол-
нения, и процессом, разбуженным системным вызовом или потоком ядра.
В первом случае планировщик прибавляет все то время, что процесс ждал в
очереди на выполнение, а во втором — только некоторую часть этого време-
времени. Дело в том, что интерактивные процессы с большей вероятностью про-
пробуждаются от асинхронных событий (представим себе пользователя, нажи-
нажимающего на клавиши), чем от синхронизированных.
Действия, выполняемые функцией scheduleQ
для переключения процессов
Итак, функция schedule о определилась, какой процесс следует выполнять
дальше. Теперь ядро обратится к структуре threadinfo процесса next, адрес
которой хранится почти в самом начале дескриптора процесса next:
switch_tasks:
prefetch(next);
Макрос prefetch заставляет управляющий блок процессора перенести содер-
содержимое первых полей дескриптора процесса next в аппаратный кэш. Он вызы-
вызывается здесь для повышения производительности функции schedule (), пото-
потому что данные будут переноситься одновременно с выполнением инструк-
инструкций, идущих следом и не затрагивающих поля дескриптора next.
Перед замещением процесса prev планировщик должен проделать опреде-
определенную организационную работу:
clear_tsk_need_resched (prev) ;
rcu_qsctr__inc (prev->thread_inf o->cpu) ;
Функция clear_tsk_need_resched() сбрасывает флаг TIF_NEED_RESCHED у про-
цесса prev на случай, если функция schedule () была вызвана "ленивым" обра-
образом. Затем функция фиксирует тот факт, что процессор временно пребывает
в состоянии покоя (см. главу 5).
Функция schedule о должна, кроме прочего, уменьшить среднее время сна
процесса prev, вычтя из него то время, что процесс пользовался процессором:
prev->sleep_avg -= run_time;
if ((long)prev->sleep avg <= 0)
prev->sleep_avg = 0;
prev->timestamp = prev->last_ran = now;
Затем у процесса обновляются отметки времени.
Вполне возможно, что prev и next являются одним и тем же процессом. Это
происходит, когда в очереди на ожидание нет другого активного процесса
с более высоким или равным приоритетом. В таком случае функция обходит
переключение процесса:
if (prev == next) {
spin_unlock_irq(&rq->lock);
goto finish_schedule;
}
Теперь планировщик находится в такой точке кода, что prev и next являются
разными процессами, и переключение необходимо:
next->timestamp = now;
rq->nr_switches++;
rq->curr = next;
prev = context_switch(rq, prev, next);
Функция contextswitch () настраивает адресное пространство процесса next.
Как мы увидим в разд. "Дескриптор памяти для потоков ядра'1 в главе 9, по-
поле activejmm дескриптора процесса указывает на дескриптор памяти, исполь-
используемой процессом, а поле mm— на дескриптор памяти, принадлежащей про-
процессу. У обычных процессов в этих полях находится один адрес, однако, у
потока ядра нет собственного адресного пространства, и его поле mm всегда
содержит null. Функция contextswitch () делает так, что если next является
потоком ядра, то он использует адресное пространство процесса prev:
if (!next->mm) {
next->active_mm = prev->active_mm;
atomic_inc (&prev->active_mm->mm_count) ;
enter_lazy_tlb(prev->active_mm, next);
}
Вплоть до версии Linux 2.2 потоки ядра имели свои адресные пространства.
Это решение не было оптимальным, поскольку приходилось менять Таблицы
Страниц каждый раз, когда планировщик выбирал новый процесс, даже если
это был поток ядра. Поскольку потоки ядра работают в режиме ядра, они ис-
используют только четвертый гигабайт пространства линейных адресов, кото-
который отображается одинаково для всех процессов в системе. Но еще хуже то,
что запись в регистр сгЗ делает недействительными все данные в TLB-
буферах (см. главу 2), а это приводит к значительному снижению производи-
производительности. Сейчас операционная система Linux гораздо эффективнее, по-
поскольку Таблицы Страниц вообще не затрагиваются, если процесс next явля-
является потоком ядра. В порядке дальнейшей оптимизации, если процесс next
является потоком ядра, функция schedule о переводит процесс в ленивый
режим TLB.
Если же next является обычным процессом, функция contextswitch () заме-
заменяет адресное пространство процесса prev адресным процессом процесса
next:
if (next->iran)
switch_mm(prev->active_ram, next->mm, next);
Если процесс prev является потоком ядра или завершает свою работу, функ-
функция contextswitch () сохраняет указатель на дескриптор памяти, используе-
используемой процессом prev, в поле prevmm структуры, определяющей очередь на
выполнение, а затем сбрасывает поле prev->active_mm:
if (! prev->mm) {
rq->prev_mm = prev->active_mm;
prev->active_mm = NULL;
}
Теперь функция contextswitcho может наконец-то вызвать макрос
switchto () для фактического переключения между процессами prev и next
(см. главу 3):
switch_to(prev, next, prev);
return prev;
Действия, выполняемые функцией scheduleQ
после переключения процесса
Инструкции функций contextswitch () и schedule о, идущие после вызова
макроса switchto, не будут тут же выполнены процессом next. Их выполнит
процесс prev позже, когда планировщик снова выберет его для выполнения.
Однако в тот момент локальная переменная prev будет указывать не на пер-
первый процесс, который подлежал замене, когда мы начали описание функции
schedule о, а на процесс, который был заменен нашим первым процессом
prev, когда планировщик снова выбрал его. Первыми инструкциями после
переключения процесса будут такие:
barrier();
finish_task_switch(prev);
Сразу после вызова функции contextswitch () из функции schedule о макрос
barrier о возвращает оптимизационный барьер для кода (см. главу 5). Затем
ВЫЗЫВаеТСЯ фуНКЦИЯ finish_task_switch () I
ram = this_rq()->prev_ram;
this_rq()->prev_mm = NULL;
prev_task_flags = prev->flags;
spin_unlock_irq(&this_rq()->lock);
if (mm)
mmdrop (mm) ;
if (prev_task_flags & PF_DEAD)
put_task_struct(prev);
Если процесс prev является потоком ядра, поле prevmm очереди на выполне-
выполнение хранит адрес дескриптора памяти, представленной процессу prev. Как мы
увидим в главе 9, функция mmdrop () уменьшает счетчик обращений дескрип-
дескриптора памяти, а если счетчик достигнет нуля (что вполне вероятно, поскольку
prev является процессом-зомби), функция освободит дескриптор вместе с ас-
ассоциированными Таблицами Страниц и областями виртуальной памяти.
Функция finishtaskswitcho также освобождает спин-блокировку очереди
на выполнение и включает локальные прерывания. Затем она проверяет, яв-
является ли prev процессом-зомби, удаляемым в этот момент из системы (см.
главу 3), и, если это действительно так, вызывает функцию puttaskstruct о,
чтобы освободить счетчик ссылок дескриптора процесса и сбросить все ос-
оставшиеся ссылки на процесс.
Заключительными инструкциями функции schedule () являются следующие:
finish_schedule:
prev = current;
if (prev->lock_depth >= 0)
reacquire_kernel_lock();
preempt_enable_no_resched();
if (test_bit(TIF_NEED_RESCHED, ¤t_thread_info()->flags)
goto need_resched;
return;
Здесь видно, что функция schedule () при необходимости заново получает
глобальную блокировку ядра, заново включает вытеснение в ядре и проверя-
проверяет, установил ли какой-нибудь другой процесс флаг tif need resched у теку-
текущего процесса. Если флаг установлен, функция schedule о выполняется еще
раз с самого начала; в противном случае она завершает работу.
Балансирование очередей на выполнение
в многопроцессорных системах
В главе 4 мы видели, что Linux придерживается модели SMP (Symmetric
Multiprocessing, симметричная многопроцессорная обработка). Это означает,
что ядро не должно предпочитать какой-то один процессор другим. Однако
многопроцессорные компьютеры изготавливаются в самых разных вариан-
вариантах, и планировщик ведет себя по-разному в зависимости от аппаратных
характеристик. В частности, мы рассмотрим три типа многопроцессорных
машин:
□ Классическая многопроцессорная архитектура— до недавнего времени
это была самая распространенная архитектура многопроцессорных ком-
компьютеров. В таких компьютерах общий набор чипов оперативной памяти
совместно используется всеми процессорами.
□ Чип с поддержкой технологии Hyper-Threading— представляет собой
микропроцессор, выполняющий несколько потоков одновременно. Он
включает в себя несколько экземпляров каждого внутреннего регистра и
быстро переключается между ними. Эта технология, изобретенная в ком-
компании Intel, позволяет процессору использовать машинные циклы для вы-
выполнения другого потока, пока текущий поток обращается к памяти. Фи-
Физический многопоточный процессор представляется операционной систе-
системе Linux в виде нескольких различных логических процессоров.
□ NUMA — чипы процессоров и оперативной памяти группируются в ло-
локальные "узлы" (обычно узел состоит из одного процессора и нескольких
чипов оперативной памяти). Арбитр памяти (специальная электронная
схема, упорядочивающая обращения к оперативной памяти от различных
процессоров в системе; см. главу 2) является узким местом для производи-
производительности классических многопроцессорных систем. В архитектуре
NUMA, когда процессор обращается к "локальному" чипу памяти внутри
своего узла, конфликты практически отсутствуют, и обращение обычно
занимает мало времени. Зато обращение к "удаленному" чипу памяти за
пределами узла происходит гораздо медленнее. В разд. "Доступ к неодно-
неоднородной памяти (NUMA)" главы 8 мы объясним, как аллокатор памяти
в ядре Linux поддерживает архитектуры NUMA.
Эти базовые типы многопроцессорных систем часто комбинируются. Напри-
Например, материнская плата с двумя процессорами с поддержкой Hyper-Threading
воспринимается ядром как четыре логических процессора.
Как мы видели в предыдущем разделе, функция schedule () выбирает новый
процесс из очереди на выполнение, принадлежащей локальному процессору.
Следовательно, каждый конкретный процессор может выполнить только вы-
выполняемые процессы, находящиеся в его очереди. С другой стороны, выпол-
выполняемый процесс всегда стоит в одной и только одной очереди на выполнение;
ни один процесс не находится в двух или более очередях. Таким образом, по-
пока процесс остается выполняемым, он обычно привязан к одному процессору.
Такой подход обычно оптимален с точки зрения производительности систе-
системы, поскольку аппаратный кэш любого процессора, вероятнее всего, содер-
содержит данные, необходимые процессам, стоящим в очереди на выполнение.
Однако в некоторых случаях привязка выполняемого процесса к конкретному
процессору может сильно понизить производительность. Рассмотрим в каче-
качестве примера случай, когда много пакетных процессоров интенсивно исполь-
используют процессор. Если большинство из них окажется в одной очереди на вы-
выполнение, то соответствующий процессор будет перегружен, в то время как
остальные будут практически простаивать.
Поэтому ядро периодически проверяет, сбалансирована ли рабочая нагрузка
в очереди на выполнение, и, если необходимо, переносит некоторые процес-
процессы из одной очереди в другую. Однако для достижения оптимальной произ-
производительности в многопроцессорной системе алгоритм балансировки нагруз-
нагрузки должен учитывать топологию организации процессоров. Начиная с версии
ядра 2.6.7, в операционной системе Linux применяется сложный алгоритм
балансирования очередей на выполнение, в основе которого лежит понятие
"области планирования". Благодаря областям планирования алгоритм может
быть легко настроен на любые существующие многопроцессорные архитек-
архитектуры (и даже самые современные, например, с "многоядерными" микропро-
микропроцессорами).
Области планирования
По своей сути область планирования является некоторым множеством про-
процессоров, рабочая нагрузка на которые балансируется ядром. Вообще говоря,
области планирования имеют иерархическую организацию: область верхнего
уровня, которая обычно распространяется на все процессоры системы, вклю-
включает в себя дочерние области, каждая из которых охватывает подмножество
процессоров. Благодаря иерархической структуре областей планирования
балансировка рабочей нагрузки происходит достаточно эффективно.
Любая область планирования разделена на одну или несколько групп, каждая
из которых представляет подмножество процессоров этой области. Баланси-
Балансирование нагрузки производится среди групп в области планирования. Иными
словами, процесс переносится с одного процессора на другой, только если
суммарная рабочая нагрузка какой-то группы в некоторой области планиро-
планирования значительно ниже рабочей нагрузки другой группы в той же области
планирования.
На рис. 7.2 изображены три примера иерархических структур областей пла-
планирования, соответствующих трем основным архитектурам многопроцессор-
многопроцессорных машин.
Рис. 7.2. Три примера иерархических структур областей планирования
На рис. 7.2, а представлена иерархическая структура, состоящая из единст-
единственной области планирования, для классической двухпроцессорной архитек-
архитектуры. Область планирования состоит из двух групп, каждая из которых
включает в себя по одному процессору.
На рис. 7.2, б изображена двухуровневая структура для двухпроцессорного
компьютера с технологией Hyper-Threading. Область планирования верхнего
уровня охватывает все четыре логических процессора и состоит из двух
групп. Каждая группа из областей верхнего уровня соответствует дочерней
области планирования и включает в себя один физический процессор. Облас-
Области планирования нижнего уровня (называемые также базовыми областями
планирования) состоят из двух групп, по одной для каждого логического про-
процессора.
На рис. 7.2, в иллюстрируется двухуровневая иерархическая структура для
восьмипроцессорной архитектуры NUMA с двумя узлами по четыре процес-
процессора в каждом. Область верхнего уровня организована в две группы, каждая
из которых соответствует отдельному узлу. Каждая базовая область планиро-
планирования охватывает процессоры внутри узла и состоит из четырех групп по од-
одному процессору.
Любая область планирования имеет дескриптор scheddomain, а любая группа
внутри нее — дескриптор schedgroup. Дескриптор scheddomain включает в
себя поле groups, указывающее на первый элемент в списке дескрипторов
групп. Кроме того, поле parent структуры scheddomain указывает на деск-
дескриптор родительской области планирования, если таковая существует.
Дескрипторы scheddomain всех физических процессоров в системе хранятся
в переменных physdomains, причем у каждого процессора своя такая пере-
переменная. Если ядро не поддерживает Hyper-Threading, эти области находятся
на нижнем уровне иерархии областей планирования, а поля sd дескрипторов
очередей на выполнение указывают на них. Иными словами — это базовые
области планирования. Если же ядро поддерживает Hyper-Threading, области
планирования нижнего уровня хранятся в процессорных переменных
cpu_domains.
Функция rebalance_tick()
ФуНКЦИЯ rebalance_tick() ВЫЗЫВаеТСЯ На КаЖДОМ ТИКе функцией scheduler_
tick о для поддержания сбалансированности очередей в системе. Она при-
принимает в качестве параметров индекс локального thiscpu, адрес локальной
очереди на выполнение thisrq и флаг idle, который может иметь следую-
следующие значения:
□ schedidle — процессор не занят, т. е. текущим является процесс swapper;
□ notidle — процессор занят, т. е. текущим является процесс, отличный от
swapper.
Функция rebaianceticko вначале определяет количество процессов в оче-
очереди на выполнение и обновляет значение средней нагрузки на эту очередь.
С этой целью функция обращается к полям nrrunning и cpuioad дескрипто-
дескриптора очереди на выполнение.
Затем функция rebaiancetick () входит в цикл по всем областям планирова-
планирования на пути от базовой области (на которую ссылается поле sd дескриптора
локальной очереди на выполнение) до области верхнего уровня. На каждом
шаге цикла функция определяет, настало ли время вызывать функцию
loadbaiance () для выполнения перебалансировки данной области планиро-
планирования. Значение параметра idle и некоторые другие значения, хранящие-
хранящиеся в дескрипторе scheddomain, определяют частоту вызовов функции
loadbaiance (). Если параметр idle равен schedidle, значит, очередь на вы-
выполнение пуста, И функция rebaiancetick () вызывает функцию load_
balance () довольно часто (приблизительно каждый тик или раз в два тика для
областей планирования, соответствующих логическим и физическим процес-
процессорам). Если, наоборот, параметр idle равен notidle, to функция
rebaiancetick () вызывает функцию loadbaiance () гораздо реже (примерно
раз в Юме для областей планирования, соответствующих логическим про-
цессорам и раз в 100 мс для областей, соответствующих физическим про-
процессорам).
Функция load_balance()
Функция loadbaiance () проверяет, является ли область планирования сильно
разбалансированной. Точнее говоря, она проверяет, можно ли уменьшить
разбалансированность, удалив несколько процессов из самой занятой группы
в очередь на выполнение, принадлежащую локальному процессору. Если та-
такое действительно возможно, функция пытается произвести миграцию про-
процессов. Она принимает четыре параметра:
□ thiscpu — индекс локального процессора;
□ thisrq — адрес дескриптора локальной очереди на выполнение;
□ sd — указатель на дескриптор проверяемой области планирования;
П idle — флаг, принимающий значение schedidle (локальный процессор не
ЗаНЯТ) ИЛИ NOT_IDLE.
Функция выполняет следующие действия:
1. Получает СПИН-блОКИрОВКу this_rq->lock.
2. Вызывает функцию findbusiestgroupO, чтобы проанализировать рабо-
рабочую нагрузку на группы внутри области планирования. Функция возвра-
возвращает адрес дескриптора schedgroup самой загруженной группы, при усло-
условии, что она не включает в себя локальный процессор; кроме того, функ-
функция возвращает количество процессов, подлежащих переносу в локальную
очередь на выполнение для восстановления баланса. В противном случае,
если самая загруженная группа включает в себя локальный процессор или
все группы сбалансированы, функция возвращает null. Это не тривиаль-
тривиальная процедура, потому что функция, по возможности, фильтрует стати-
статистические флуктуации рабочей нагрузки.
3. Если функция f indbusiestgroup () не нашла группу, которая не содержит
локальный процессор и загружена значительно сильнее других групп в
этой области планирования, описываемая функция освобождает спин-
блокировку this_rq->iock, настраивает параметры дескриптора области
планирования так, чтобы отсрочить следующий вызов функции
loadbaiance () на локальном процессоре, и завершает работу.
4. Вызывает функцию find_busiest_queue(), чтобы найти самые загружен-
загруженные процессоры в группе, найденной на шаге 2. Функция возвращает ад-
адрес дескриптора busiest соответствующей очереди на выполнение.
5. Получает вторую спин-блокировку, а именно busiest->iock. Чтобы избе-
избежать взаимных блокировок, это нужно делать аккуратно: вначале освобо-
ждается this_rq->iock, а затем обе блокировки захватываются в порядке
возрастания индекса процессора.
6. Вызывает функцию movetaskso в попытке перенести некоторые процес-
процессы ИЗ ОЧереДИ busiest В локальную ОЧереДЬ this_rq.
7. Если функции movetasks о не удалось произвести миграцию процессов в
локальную очередь на выполнение, область планирования осталась разба-
лансированной. Функция устанавливает флаг busiest->active_baiance и
будит поток ядра migration, дескриптор которого хранится в поле busiest->
migrationthread. Поток migration проходит по всей цепочке областей
планирования от базовой области, соответствующей очереди busiest, до
области самого верхнего уровня и ищет свободный процессор. Если такой
процессор обнаруживается, поток ядра вызывает функцию movetasks о,
чтобы перенести один процесс в свободную очередь на выполнение.
8. Освобождает СПИН-блОКИрОВКИ busiest->lock И this_rq->lock.
9. Завершает работу.
Функция move_tasks()
Функция movetasks о переносит процессы из исходной очереди на выполне-
выполнение в локальную. Она принимает шесть параметров: thisrq и thiscpu (де-
(дескриптор локальной очереди и индекс локального процессора), busiest (деск-
(дескриптор исходной очереди), maxnrmove (максимальное количество переноси-
переносимых процессов), sd (адрес дескриптора области планирования, в которой
происходит данная операция балансировки) и флаг idle (кроме значений
sched_idle и not_idle, он может получить значение newly_idle, когда функ-
функция вызывается неявно функцией idle_balance () ).
Функция вначале просматривает процессы с истекшими квантами времени в
очереди на выполнение busiest, начиная с тех, у которых приоритет выше.
Перебрав все процессы с истекшими квантами времени, функция переходит к
просмотру активных процессов в той же очереди busiest. Для каждого про-
процесса-кандидата на перенос функция вызывает функцию can_migrate_task(),
которая возвращает 1, если выполнены все перечисленные условия:
□ процесс не выполняется в данный момент на другом процессоре;
□ локальный процессор включен в битовую маску cpusai lowed дескриптора
процесса;
□ выполнено одно из следующий условий:
• локальный процессор свободен. Если ядро поддерживает Hyper-
Threading, все логические процессоры локального физического процес-
процессора должны быть свободны;
• ядро испытывает проблемы с балансировкой области планирования,
потому что многократные попытки переместить процессы закончились
неудачно;
• процесс, подлежащий переносу, не является "горячим в кэше" (он не
выполнялся в последнее время на удаленном процессоре, и можно
предположить, что в аппаратном кэше удаленного процессора нет дан-
данных этого процесса).
ЕСЛИ функция canmigratetaskO возвращает 1, функция move_tasks() ВЫЗЫ-
вает функцию puiitasko, чтобы перенести процесс-кандидат в локальную
очередь на выполнение. Говоря кратко, функция puiitasko вызывает функ-
функцию dequeuetasko, чтобы убрать процесс из удаленной очереди на выпол-
выполнение, затем вызывает функцию enqueuetasko, чтобы поставить его в ло-
локальную очередь, и, наконец, если у перемещенного процесса динамический
приоритет выше, чем у процесса current, вызывает функцию reschedtasko,
чтобы вытеснить текущий процесс локального процессора.
Системные вызовы,
относящиеся к планированию
Чтобы позволить процессам менять свои приоритеты и выбирать политику
планирования, было создано несколько системных вызовов. В порядке обще-
общего правила, пользователям всегда разрешено понижать приоритеты своих
процессов. Если же они захотят изменить приоритеты процессов, принадле-
принадлежащих другим пользователям или повысить приоритеты своих процессов, то
им потребуются привилегии суперпользователя.
Системный вызов nice()
Системный вызов nice () (от англ. "приятный, вежливый"K позволяет процес-
процессу менять базовый приоритет. Целое значение, передаваемое с параметром
increment, служит для корректировки поля nice дескриптора процесса.
Команда nice операционной системы Unix, позволяющая пользователям за-
запускать программы с модифицированным приоритетом планирования, осно-
основана на этом системном вызове.
Служебная процедура sysnice () обрабатывает системный вызов nice (). Хо-
Хотя параметр increment может иметь любое значение, значения, превышаю-
3 Поскольку этот системный вызов обычно делается для понижения приоритета процесса, пользо-
пользователи, прибегающие к нему, совершают поступок, "приятный" остальным пользователям, действу-
действуют "вежливо" по отношению к ним.
щие по абсолютной величине 40, усекаются до 40. По традиции отрицатель-
отрицательные значения соответствуют запросам на повышение приоритета и требуют
суперпользовательских привилегий, а положительные обозначают понижение
приоритета. Если параметр отрицательный, служебная процедура вызывает
функцию capable о, чтобы проверить, есть ли у процесса способность
CAPSYSNICE. Кроме ТОГО, ВЫЗЫВаеТСЯ фуНКЦИЯ-ПереХВаТЧИК security_task_
setnice (). Эта функция обсуждается в главе 20. Если окажется, что пользова-
пользователь обладает привилегией, необходимой для изменения приоритетов, слу-
служебная процедура sysniceo преобразует значение поля current->static_
prio в интервал nice-значений, прибавляет значение параметра increment и
вызывает функцию set_user_nice(). Эта функция получает блокировку на
локальную очередь на выполнение, обновляет статический приоритет теку-
текущего процесса, вызывает функцию reschedtasko, чтобы позволить другим
процессам вытеснить текущий, и освобождает блокировку очереди на выпол-
выполнение.
Системный вызов nice о поддерживается исключительно в целях обратной
совместимости. Он уже заменен системным вызовом setpriority (), описан-
описанным в следующем разделе.
Системные вызовы getpriorityO и setpriorityQ
Системный вызов nice о затрагивает только процесс, который его сделал.
Два других системных вызова, getpriority () и setpriority (), работают с ба-
базовыми приоритетами всех процессов в данной группе. Вызов getpriority ()
возвращает 20 минус наименьшее значение полей nice всех приоритетов в
этой группе, т. е. наивысший приоритет среди этих процессов. Вызов
setpriority о устанавливает в заданное значение базовый приоритет всех
процессов в указанной группе.
Ядро реализует эти системные вызовы при помощи служебных процедур
sysgetpriorityo и syssetpriorityо. Обе они принимают практически
одинаковые наборы параметров:
□ which — значение, идентифицирующее группу процессов, может быть од-
одним из следующих:
• prioprocess — процессы выбираются по идентификаторам процессов
(поле pid дескриптора процесса);
• priopgrp— процессы выбираются по идентификаторам групп (поле
pgrp дескриптора процесса);
• priouser — процессы выбираются по идентификаторам пользователей
(поле uid дескриптора процесса);
□ who — значение поля pid, pgrp или uid (в зависимости от значения пара-
параметра which), используемое для выбора процесса. Если параметр who
равен 0, он устанавливается равным соответствующему полю процесса
current;
□ niceval — новый базовый приоритет (необходим только функции sys_
setpriorityo). Он должен лежать в диапазоне от-20 (наивысший при-
приоритет) до +19 (самый низкий).
Как было сказано ранее, только процессам со способностью capsysnice
разрешено повышать свой базовый приоритет или изменять приоритеты дру-
других процессов.
В главе 10 мы увидим, что системные вызовы возвращают отрицательные
значения только при возникновении ошибки. По этой причине вызов
getpriority () возвращает не нормальное nice-значение из диапазона от -20
до +19, а неотрицательное значение от 1 до 40.
Системные вызовы sched_getaffinity()
и sched_setaffinity()
Системные ВЫЗОВЫ sched_getaffinity() И sched_setaffinity () возвращают И
устанавливают, соответственно, маску привязки процессора к процессу, т. е.
битовую маску процессоров, которым разрешено выполнять этот процесс.
Эта маска хранится в поле cpusaiiowed дескриптора процесса.
Служебная процедура sysschedgetaffinityo ищет дескриптор процесса с
помощью функции findtaskbypido, а затем возвращает результат опера-
операции ЛОГИЧЕСКОЕ И над соответствующим полем cpusai lowed и битовой
маской доступных процессоров.
Системный вызов sysschedsetaffinityo устроен чуть сложнее. Помимо
поиска дескриптора целевого процессора и обновления поля cpusaiiowed, эта
функция должна проверить, стоит ли процесс в очереди на выполнение у
процессора, который отсутствует в обновленной маске привязки. В худшем
случае, придется перенести процесс в другую очередь на выполнение. Чтобы
избежать проблем со взаимными блокировками и попытками одновременно-
одновременного обращения, эта работа перекладывается на потоки ядра migration (такой
поток есть у каждого процессора). Если процесс подлежит переносу из оче-
очереди rqi в очередь rq2, системный вызов пробуждает поток migration очереди
rql (rql->migration_thread), КОТОрЫЙ удаляет процесс ИЗ rql И СТаВИТ его В
очередь на выполнение rq2.
Системные вызовы, относящиеся к процессам
реального времени
Здесь мы представим читателю группу системных вызовов, позволяющих
процессу менять его дисциплину планирования и, в частности, становиться
процессом реального времени. Как всегда, процесс должен иметь способ-
способность CAPSYSNICE, ЧТОбы Модифицировать ЗНачеНИЯ ПОЛеЙ rt_priority И
policy у дескриптора любого процесса, включая собственный.
Системные вызовы sched_getscheduler()
и sched_setscheduler()
Системный вызов schedgetscheduier () запрашивает политику планирования,
действующую в отношении процесса, идентифицируемого параметром pid.
Если pid равен 0, считывается политика вызвавшего процесса. В случае успе-
успеха системный вызов возвращает политику schedfifo, schedrr или
schednormal (последняя также называется schedother). Соответствующая
служебная процедура sysschedgetscheduier () вызывает функцию
f indprocessbypid (), которая находит дескриптор процесса по переданному
значению pid и возвращает значение его поля policy.
Системный вызов schedsetscheduier о устанавливает как политику плани-
планирования, так и соответствующие параметры для процесса, идентифицируемо-
идентифицируемого параметром pid. Если pid равен 0, устанавливаются параметры планиров-
планировщика, применяемые к вызвавшему процессу.
Соответствующая служебная процедура sysschedsetscheduier о просто вы-
вызывает функцию doschedsetscheduier (). Эта функция проверяет допусти-
допустимость политики планирования, определяемой параметром policy, и нового
приоритета, определяемого параметром param->sched_priority. Она также
проверяет, есть ли у процесса способность capsysnice, или наличие прав
суперпользователя у его владельца. Если все в порядке, она удаляет процесс
из очереди на выполнение (если он выполняемый), обновляет статический и
динамический приоритеты и приоритет реального времени у процесса, воз-
возвращает процесс в очередь на выполнение и, если необходимо, вызывает
функцию reschedtasko для вытеснения текущего процесса, принадлежаще-
принадлежащего данной очереди.
Системные вызовы sched_getparam() и sched_setparam()
Системный вызов sched_getparam() читает параметры процесса, идентифици-
идентифицируемого параметром pid. Если pid равен 0, считываются параметры процесса
current. Соответствующая служебная процедура sys_sched_getparam(), как и
следует ожидать, находит указатель на дескриптор процесса по параметру
pid, сохраняет поле rtpriority в локальной переменной типа schedparam и
вызывает функцию copytouser (), чтобы скопировать это значение в адрес-
адресное пространство процесса, по адресу, заданному параметром param.
Системный ВЫЗОВ sched_setparam() аналогичен вызову sched_setscheduler().
Различие состоит в том, что schedsetparamo не позволяет вызвавшему про-
процессу задавать значение поля policy4. Соответствующая служебная процеду-
процедура sys_sched_setparam() вызывает функцию do_sched_setscheduler () Практи-
чески с теми же параметрами, что и служебная процедура sys_sched_
setscheduler().
Системный вызов sched_yield()
Системный вызов schedyieido позволяет процессу добровольно освободить
процессор без приостановки своего выполнения. Процесс остается в состоя-
состоянии taskrunning, а планировщик заносит его либо в набор процессов с ис-
истекшими квантами времени (если это обычный процесс), либо в конец списка
в очереди на выполнение (если это процесс реального времени). Затем вызы-
вызывается функция schedule (). В результате у других процессов с тем же дина-
динамическим приоритетом появляется возможность поработать. Данный вызов
используется, в основном, процессами реального времени, принадлежащими
КЛаССу SCHED_FIFO.
Системные вызовы sched_get_priority_min()
и sched_get_priority_max()
Системные ВЫЗОВЫ sched get priority_min () И sched_get_priority max() BO3-
вращают, соответственно, минимальный и максимальный статический при-
приоритет реального времени, который может быть использован при проведении
политики планирования, идентифицируемой параметром policy.
Служебная процедура sysschedgetprioritymino возвращает 1, если
current является процессом реального времени, и 0 в противном случае.
Служебная процедура sysschedgetprioritymax () возвращает 99 (наивыс-
(наивысший приоритет), если current является процессом реального времени, и О
в противном случае.
Системный вызов sched_rr_getjnterval()
Системный вызов sched_rr_get_intervai () записывает в структуру, храня-
хранящуюся в адресном пространстве режима пользователя, квант времени, соот-
4 Эта аномалия вызвана особым требованиям стандарта POSIX.
ветствующий круговому принципу работы, для процесса реального времени,
идентифицируемого параметром pid. Если pid равен 0, системный вызов за-
записывает квант времени текущего процесса.
Как обычно, Соответствующая Служебная Процедура sys_sched_rr_get_
interval () ВЫЗЫВаеТ фуНКЦИЮ find_process_by_pid(), чтобы ПОЛуЧИТЬ ДеСК-
риптор процесса по значению pid. Затем она преобразует базовый квант вре-
времени выбранного процесса в секунды и наносекунды и копирует эти числа в
структуру пользовательского режима. В соответствии с соглашением, вре-
временной квант процесса реального времени, принадлежащего классу "первым
вошел — первым вышел", равен нулю.
ГЛАВА 8
Управление памятью
В главе 2 мы видели, как Linux пользуется сегментацией в архитектуре 80x86
и блоками управления памятью для трансляции логических адресов в физи-
физические. Мы также говорили о том, что определенная часть оперативной памя-
памяти постоянно выделена ядру и служит для хранения кода ядра и статических
структур с данными ядра.
Оставшаяся память называется динамической. Это ценный вычислительный
ресурс, необходимый не только процессам, но и самому ядру. Фактически,
производительность всей системы зависит от эффективности управления ди-
динамической памятью. Поэтому все современные многозадачные операцион-
операционные системы пытаются оптимизировать использование оперативной памяти,
выделяя ее только в случае необходимости и освобождая, как только это ста-
становится возможным. На рис. 8.1 схематически представлены страничные
кадры, используемые в качестве динамической памяти.
В этой главе, состоящей из трех основных разделов, описывается, как ядро
выделяет динамическую память под свои нужды. В разд. "Управление стра-
страничными кадрами" и "Управление областями памяти" обсуждаются два раз-
разных подхода к работе с физическими непрерывными областями памяти, а
разд. "Управление несмежными областями памяти" иллюстрирует технику
работы с областями, состоящими из несмежных страничных кадров. В этих
разделах мы затронем такие темы, как зоны памяти, отображения ядра,
buddy-система, slab-кэш и пулы памяти.
Управление страничными кадрами
В главе 2 мы видели, как процессор Intel Pentium может работать со странич-
страничными кадрами двух разных размеров 4 Кбайт и 4 Мбайт (или 2 Мбайт, если
Рис. 8.1. Динамическая память
включен механизм РАЕ). В операционной системе Linux в качестве стандарт-
стандартной единицы выделения памяти выбран размер страничного кадра 4 Кбайт.
Это упрощает управление памятью по двум причинам:
□ исключения "ошибка обращения к странице", возбуждаемые схемами
управления памятью, легко интерпретируются. Либо запрошенная страни-
страница существует, но процессу запрещено к ней обращаться, либо страница
отсутствует. Во втором случае аллокатор памяти должен выделить стра-
страничный кадр размером в 4 Кбайт и присвоить его процессу;
□ хотя значения 4 Кбайт и 4 Мбайт и кратны размеру любого блока на диске,
обмен данными между основной памятью и дисками в большинстве слу-
случаев происходит эффективнее при использовании меньшего размера.
Дескрипторы страниц
Ядро должно отслеживать текущий статус каждого страничного кадра. На-
Например, оно должно уметь отличать страничные кадры, содержащие страни-
страницы процессов, от страничных кадров с кодом или данными ядра. Аналогич-
Аналогичным образом оно должно уметь определять, свободен ли страничный кадр,
находящийся в динамической памяти. Страничный кадр в динамической па-
памяти свободен, если он не содержит полезных данных. Он, наоборот, не сво-
свободен, когда содержит данные процесса, работающего в режиме пользовате-
пользователя, данные программного кэша, динамически выделенные структуры данных
ядра, данные в буфере драйвера устройства, код модуля ядра и т. д.
Информация о состоянии страничного кадра хранится в дескрипторе страни-
страницы, имеющем тип page, поля которого перечислены в табл. 8.1. Все дескрип-
дескрипторы страниц образуют массив memmap. Поскольку каждый дескриптор имеет
длину 32 байта, массив memmap занимает менее 1% всей оперативной памяти.
Макрос virttopage(addr) возвращает адрес дескриптора страницы, соот-
ветствующего линейному адресу addr. Макрос pfntopage (pfn) возвращает
адрес дескриптора страницы, ассоциированного со страничным кадром,
имеющим номер pfn.
Таблица 8.1. Поля дескриптора страницы
Тип Имя Описание
unsigned long flags Массив флагов (см. табл. 8.2). Кроме прочего,
кодирует номер зоны, которой принадлежит
страничный кадр
atomic_t _count Счетчик ссылок страничного кадра
atomic_t _mapcount Количество записей Таблицы Страниц, ссы-
ссылающихся на страничный кадр (-1, если таких
записей нет)
unsigned long private Это поле доступно компоненту ядра, пользую-
пользующемуся данной страницей (например, оно яв-
является указателем на голову буфера у страни-
страницы буферов; см. разд. "Блочные буферы и го-
головы буферов" в главе 15). Когда страница
свободна, поле используется buddy-системой
struct address_space * mapping Это поле используется, когда страница зано-
заносится в кэш страниц (см. разд. "Кэш страниц"
в главе 15), или когда страница принадлежит
анонимной области памяти (см. разд. "Об-
"Обратное отображение для анонимных стра-
страниц" в главе 17)
unsigned long index Это поле по-разному используется различными
компонентами ядра. Например, оно идентифи-
идентифицирует позицию данных, хранящихся в стра-
страничном кадре в рамках образа страницы на
диске или в пределах анонимной области па-
памяти (см. главу 15); в другом случае оно со-
содержит идентификатор выгруженной страницы
(см. главу 17)
struct listhead lru Поле содержит указатели на двунаправленный
список давно неиспользуемых страниц
Читателю необязательно вникать в назначение всех полей дескриптора стра-
страницы прямо сейчас. В последующих главах мы будем неоднократно возвра-
возвращаться к полям дескриптора страниц. Кроме того, некоторые поля могут не-
нести разный смысл, в зависимости от того, свободен ли страничный кадр, и
какой компонент ядра его использует.
Мы подробно опишем два поля:
□ count— счетчик обращений к странице. Если он равен-1, соответст-
соответствующий страничный кадр свободен и может быть присвоен любому про-
цессу или самому ядру. Если счетчик имеет значение, большее или рав-
равное 0, страничный кадр присвоен одному или нескольким процессам или
содержит какие-либо структуры данных ядра. Функция pagecount () воз-
возвращает значение поля count, увеличенное на единицу, т. е. количество
пользователей этой страницы;
□ flags — включает в себя до 32 флагов (табл. 8.2), характеризующих со-
состояние страничного кадра. Для каждого флага PGxyz в ядре определены
макросы для манипуляций со значениями флага. Обычно макрос PageXyz
возвращает состояние флага, а макросы setPageXyz и ciearPageXyz уста-
устанавливают и сбрасывают нужный бит, соответственно.
Таблица 8.2. Флаги состояния страничного кадра
Имя флага Значение
PGlocked Страница заблокирована; например, она участвует в дисковой
операции ввода/вывода
PG_error При передаче страницы возникла ошибка ввода/вывода
PG_ref erenced Недавно имело место обращение к странице
PG_uptodate Флаг устанавливается по окончании операции чтения,
если не произошла ошибка ввода/вывода
PG_dirty Страница подверглась изменениям (см. разд. "Реализация
алгоритма PFRA" в главе 17)
PG_lru Страница находится в списке активных или неактивных страниц
(см. разд. "Списки давно неиспользуемых страниц (LRU)"
в главе 17)
PG_active Страница находится в списке активных страниц (см. разд. "Списки
давно неиспользуемых страниц (LRU)" в главе 17)
PG_slab Страничный кадр включен в участок памяти (см. разд. "Управление
областями памяти" далее в этой главе)
PG_highmem Страничный кадр принадлежит зоне zone_highmem (cm. разд. "Дос-
"Доступ к неоднородной памяти (NUMA)" далее в этой главе)
PG_checked Используется в некоторых файловых системах, например, Ext2 и
Ext3 (см. главу 18)
PG_arch_l He используется в архитектуре 80x86
PG_reserved Страничный кадр зарезервирован под код ядра или
не используется
PGprivate Поле private дескриптора страницы содержит осмысленные данные
PG_writeback Страница записывается на диск методом writepage (см. главу 16)
PG_nosave Используется для приостановки/возобновления работы системы
Таблица 8.2 (окончание)
Имя флага Значение
PG_compound Страничный кадр обрабатывается механизмом расширенного
выделения страниц (см. главу 2)
PGswapcache Страница принадлежит кэшу выгрузки (см. разд. "Кэш выгрузки"
в главе 17)
PGjnappedtodisk Все данные в страничном кадре соответствуют блокам, выделен-
выделенным на диске
PG_reclaim Страница помечена как подлежащая записи на диск с целью
утилизации памяти
PG_nosave_fгее Используется для приостановки/возобновления работы системы
Доступ к неоднородной памяти (NUMA)
Мы привыкли считать память компьютера однородным общедоступным ре-
ресурсом. Если не принимать во внимание роль аппаратных кэшей, мы полага-
полагаем, что время, необходимое процессору для обращения к ячейке памяти,
практически не зависит от физического адреса ячейки и от конкретного про-
процессора. К сожалению, в некоторых архитектурах это предположение невер-
неверно. Например, дела обстоят не так в отношении некоторых многопроцессор-
многопроцессорных компьютеров Alpha или MIPS.
Linux 2.6 поддерживает модель NUMA (Non-Uniform Memory Access, Доступ
к неоднородной памяти), в которой время обращения данного процессора к
разным ячейкам памяти может варьироваться. Физическая память системы
разделяется на несколько узлов. Время, необходимое данному процессору
для обращения к страницам в пределах одного узла, одинаково. Однако оно
может быть разным у двух различных процессоров. Для каждого процессора
ядро старается минимизировать количество обращений к "дорогим" узлам,
тщательно следя за тем, где хранятся данные ядра, наиболее часто
запрашиваемые этим процессором1.
Физическая память внутри каждого узла может быть разбита на несколько
зон, в чем мы убедимся в следующем разделе. У каждого узла есть дескрип-
дескриптор типа pgdatat. Поля этого дескриптора перечислены в табл. 8.3. Все де-
дескрипторы узлов собраны в однонаправленный список, на первый элемент
которого указывает переменная pgdatiist.
1 Более того, ядро Linux использует модуль NUMA даже в некоторых специфических однопроцес-
однопроцессорных системах, имеющих большие "дыры" в физическом адресном пространстве. Ядро работает с
такими архитектурами, присваивая смежные диапазоны допустимых физических адресов различным
узлам памяти.
Таблица 8.3. Поля дескриптора узла
Тип Имя Описание
struct zone [ ] node_zones Массив дескрипторов зон узла
struct zonelist [ ] node_zonelists Массив структур zonelist, исполь-
используемых аллокатором страниц (см.
разд. "Зоны памяти" далее в этой
главе)
int nr_zones Количество зон в узле
struct page * node_mem_map Массив дескрипторов страниц узла
struct bootmem_data * bdata Поле, используемое ядром на этапе
инициализации
unsigned long node_start_pfn Индекс первого страничного кадра в
узле
unsigned long node_present_pages Размер узла памяти (в страничных
кадрах), без учета "дыр"
unsigned long node_spanned_pages Размер узла памяти (в страничных
кадрах), с учетом "дыр"
int node_id Идентификатор узла
pg_data_t * pgdat_next Следующий элемент списка узлов
памяти
wait_queue_head_t kswapd_wait Очередь ожидания для демона
kswapd (см. разд. "Периодическая
утилизация" в главе 17)
struct task_struct * kswapd Указатель на дескриптор процесса,
являющегося потоком ядра kswapd
int kswapd_max_order Логарифмический размер свободных
блоков, которые должен создать по-
поток kswapd
Нас, как обычно, в первую очередь, интересует архитектура 80x86. В IBM-
совместимых компьютерах применяется модель UMA (Uniform Memory
Access, Доступ к однородной памяти), и, следовательно, поддержка модели
NUMA не требуется. Однако если поддержка NUMA не компилирована в яд-
ядро, Linux будет использовать единственный узел, включающий в себя всю
физическую память системы. Таким образом, переменная pgdatiist будет
указывать на список из одного элемента (дескриптора узла 0), хранящийся
В переменной contig_page_data.
Группирование физической памяти в одном узле в архитектуре 80x86 может
показаться бесполезным делом, но такой подход делает код управления па-
мятью переносимым на другие платформы, поскольку ядро может предпола-
предполагать, что физическая память разделена на один или несколько узлов в любой
архитектуре2.
Зоны памяти
В идеальной архитектуре компьютера страничный кадр является единицей
памяти, которую можно использовать как угодно: для хранения пользова-
пользовательских данных и данных ядра, для буферизации данных с диска и т. д.
Страница данных любого типа может быть сохранена в страничном кадре без
каких-либо ограничений.
Однако реальные компьютерные архитектуры имеют аппаратные ограниче-
ограничения, которые могут повлиять на способы использования страничных кадров.
В частности, ядро Linux может столкнуться с двумя аппаратными ограниче-
ограничениями в архитектуре 80x86:
□ процессоры DMA для старой шины ISA имеют очень сильное ограниче-
ограничение, они могут обращаться только к первым 16 Мбайт оперативной па-
памяти;
□ в современных 32-разрядных компьютерах с огромными объемами опера-
оперативной памяти процессор не может напрямую обращаться ко всей физиче-
физической памяти из-за того, что линейное адресное пространство слишком
мало.
Чтобы преодолеть эти два ограничения, в Linux 2.6 физическая память каж-
каждого узла памяти разделена на три зоны. В архитектуре 80x86 с моделью
UMА эти зоны такие:
□ zonedma— содержит страничные кадры памяти ниже 16 Мбайт;
□ zonenormal— содержит страничные кадры памяти от 16 Мбайт (включи-
(включительно) до 896 Мбайт;
□ zonehighmem — содержит страничные кадры памяти от 896 Мбайт (вклю-
(включительно) и выше.
Зона zonedma содержит страничные кадры, к которым старые устройства на
основе шины ISA могут обращаться средствами DMA (см. разд. "Прямой
доступ к памяти (DMA)" главы 13).
Зоны zonedma и zonenormal включают в себя "нормальные" страничные кад-
кадры, к которым ядро может обратиться напрямую, при помощи линейного
2 Мы видели другой пример подобного конструктивного решения: в Linux применяется четыре
уровня Таблиц Страниц, даже если аппаратная архитектура ограничивается двумя (см. разд. "Paging
in Lima" в главе 2).
отображения в четвертый гигабайт пространства линейных адресов (см. гла-
главу 2). Зато zonehighmem содержит страничные кадры, к которым ядро не мо-
может обратиться напрямую, при помощи линейного отображения в четвертый
гигабайт пространства линейных адресов (см. разд. Отображение ядром
страничных кадров верхней памяти" далее в этой главе), В 64-разрядных
архитектурах зона zone_highmem всегда пуста.
Каждая зона памяти имеет дескриптор типа zone. Его поля перечислены в
табл. 8.4.
Таблица 8.4. Поля дескриптора зоны
Тип Имя Описание
unsigned long f ree_pages Количество свободных страниц
в зоне
unsigned long pagesmin Количество зарезервированных
страниц зоны (см. разд. "Пул заре-
зарезервированных страничных кадров"
далее в этой главе)
unsigned long pages_low Нижняя отметка для утилизации
страничных кадров; кроме того,
используется аллокатором зон
в качестве порогового значения (см.
разд. "Аллокатор зон" далее в этой
главе)
unsigned long pages_high Верхняя отметка для утилизации
страничных кадров; кроме того, ис-
используется аллокатором зон в каче-
качестве порогового значения
unsigned long [] lowmem_reserve Определяет, сколько страничных
кадров должно быть зарезервирова-
зарезервировано для критических ситуаций "дефи-
"дефицит памяти"
struct per_cpuj?ageset [] pageset Структура, применяемая при реали-
реализации специальных кэшей одиночных
страничных кадров (см. разд. "Кэш
страничных кадров процессора" да-
далее в этой главе)
spinlock_t lock Спин-блокировка, защищающая де-
дескриптор
struct free_area [] free_area Идентифицирует блоки свободных
страничных кадров в зоне (см.
разд. "Алгоритм "buddy-система""
далее в этой главе)
spinlock_t lru_lock Спин-блокировка для активных и
неактивных списков
Таблица 8.4 (продолжение)
Тип Имя Описание
struct list head activelist Список активных страниц в зоне (см.
главу 17)
struct list head inactive_list Список неактивных страниц в зоне
(см. главу 17)
unsigned long nr_scan_active Количество активных страниц, под-
подлежащих сканированию при утилиза-
утилизации памяти (см. разд. "Утилизация
при дефиците памяти" в главе 17)
unsigned long nr_scan_inactive Количество неактивных страниц,
подлежащих сканированию при
утилизации памяти
unsigned long nr_active Количество страниц в активном
списке зоны
unsigned long nrinactive Количество страниц в неактивном
списке зоны
unsigned long pages_scanned Счетчик, используемый при утилиза-
утилизации страничных кадров зоны
int all_unreclaimable Флаг, устанавливаемый, когда зона
заполнена страницами, не подлежа-
подлежащими утилизации
int temppriority Временный приоритет зоны (исполь-
(используемый при утилизации страничных
кадров зоны)
int prev_priority Приоритет зоны от 12 до 0 (исполь-
(используется алгоритмом утилизации стра-
страничных кадров (см. разд. "Ути-
"Утилизация при дефиците памяти"
в главе 17)
wait_queue_head_t * wait_table Хеш-таблица очередей процессов,
ожидающих одну из страниц зоны
unsigned long wait_table_size Размер хеш-таблицы очередей ожи-
ожидания
unsigned long wait_table_bits Порядок (степень двойки) размера
массива хеш-таблицы очередей ожи-
ожидания
struct pglist_data * zone_pgdat Узел памяти
struct page * zonejnemmap Указатель на первый дескриптор
страницы в зоне
unsigned long zone_start_pfn Индекс первого страничного кадра
зоны
Таблица 8.4 (окончание)
Тип Имя Описание
unsigned long spanned_pages Общий размер зоны в страницах,
включая "дыры"
unsigned long present_pages Общий размер зоны в страницах, не
включая "дыры"
char * name Указатель на общепринятое назва-
название зоны: "DMA" (прямой доступ к
памяти), "Normal" (нормальная) или
"HighMem" (верхняя память)
Многие поля структуры zone используются при утилизации страничных кад-
кадров и будут описаны в главе 17.
Каждый дескриптор страницы имеет ссылки на узел памяти и на зону внутри
узла, где содержится соответствующий страничный кадр. Для экономии мес-
места эти ссылки хранятся не как классические указатели, а закодированы в виде
индексов и находятся в старших битах поля flags. Поскольку количество
флагов, характеризующих страничный кадр, ограничено, всегда имеется воз-
возможность зарезервировать старшие биты поля flags для кодирования узла
памяти и номера зоны3. Функция page_zone() принимает в качестве парамет-
параметра адрес дескриптора страницы. Она читает старшие биты поля flags этого
дескриптора, а затем определяет адрес соответствующего дескриптора зоны,
просматривая массив zonetabie. Этот массив инициализируется на этапе за-
загрузки системы; в него записываются адреса всех дескрипторов зон всех
узлов памяти.
Когда ядро вызывает функцию выделения памяти, оно должно указать зоны,
содержащие запрошенные страничные кадры. Ядро обычно сообщает, какие
зоны оно хочет использовать. Например, если страничный кадр должен быть
напрямую отображен в четвертый гигабайт линейных адресов, но не будет
использован в пересылке данных средствами DMA с использованием шины
ISA, то ядро запрашивает страничный кадр либо в зоне zonenormal, либо в
зоне zonedma. Конечно, страничный кадр должен быть выделен в зоне
zonedma, только если в зоне zonenormal нет свободных страничных кадров.
3 Количество битов, зарезервированных под индексы, зависит от того, поддерживает ли ядро мо-
модель NUMA, и от размера flags. Если модель NUMA не поддерживается, поле flags имеет два
бита под индекс зоны и один бит (всегда равный нулю) под индекс узла. На 32-разрядных архитек-
архитектурах с поддержкой NUMA поле flags имеет два бита под индекс зоны и шесть битов под номер
узла. На 64-разрядных архитектурах с поддержкой NUMA поле flags имеет два бита под индекс
зоны и десять битов под номер узла.
Чтобы задать предпочтительные зоны в запросе на выделение памяти, ядро
пользуется структурой zoneiist, являющейся массивом указателей на деск-
дескрипторы зон.
Пул зарезервированных страничных кадров
Запросы на выделение памяти можно удовлетворять двумя разными спосо-
способами. Если в наличии имеется достаточно свободной памяти, запрос может
быть удовлетворен немедленно. В противном случае должна быть выполнена
утилизация памяти, и управляющий тракт ядра, сделавший запрос, блокиру-
блокируется до появления свободной памяти.
Однако некоторые управляющие тракты ядра не могут быть блокированы,
когда они запрашивают память. Например, такая ситуация имеет место при
обработке прерывания или при выполнении кода в критической области.
В этих случаях поток ядра должен выдавать атомарные запросы на выделе-
выделение памяти (с использованием флага gfpatomic). Атомарный запрос никогда
не блокируется: если свободных страниц недостаточно, выделение памяти
завершается неудачей.
Хотя нет никакой гарантии, что атомарный запрос на выделение памяти ни-
никогда не закончится неудачей, ядро старается минимизировать вероятность
этого нежелательного события. С этой целью оно резервирует пул странич-
страничных кадров для атомарных запросов на память, который используется только
в условиях дефицита памяти.
Объем зарезервированной памяти (в килобайтах) хранится в переменной
minf reekbytes. Ее начальное значение устанавливается при инициализации
ядра и зависит от объема физической памяти, непосредственно отображенной
в четвертый гигабайт линейных адресов ядра, т. е. значение зависит от коли-
количества страничных кадров в зонах памяти zonedma и zonenormal:
размер зарезервированного пула = Vl 6 х п (килобайт),
где п — напрямую отображенная память.
Впрочем, начальное значение переменной minfreekbytes не может быть
меньше 128 и больше 65 5364.
Зоны памяти zonedma и zonenormal "жертвуют" под зарезервированную па-
память количество страничных кадров, пропорциональное их размерам. На-
Например, если зона zonenormal в восемь раз больше зоны zonedma, to семь
4 Объем зарезервированной памяти может быть впоследствии изменен системным администрато-
администратором в файле /proc/sys/vm/min_free_kbytes или с помощью системного вызова sysctl ().
восьмых зарезервированных страничных кадров берется из zonenormal, а од-
одна восьмая — из zone_dma.
Поле pagesmin дескриптора zone содержит количество зарезервированных
страничных кадров внутри зоны. Как мы увидим в главе 17, это поле играет
свою роль в алгоритме утилизации страничных кадров (совместно с полями
pageslow и pageshigh). Значение в поле pagesiow всегда равно 5/4 от значе-
значения в поле pagesmin, а значение pageshigh всегда равно 3/2 от значения в
ПОЛе pages_min.
Зонный аллокатор страничных кадров
Подсистема ядра, обрабатывающая запросы на выделение памяти, имеющие
отношение к группам смежных страничных кадров, называется зонным алло-
катором страничных кадров. Основные компоненты этого аллокатора изо-
изображены на рис. 8.2.
Рис. 8.2. Компоненты зонного аллокатора страничных кадров
Компонент под названием "аллокатор зон" получает запросы на выделение и
освобождение динамической памяти. Приняв запрос на выделение памяти, он
ищет зону, содержащую группу смежных страничных кадров, которая могла
бы удовлетворить запрос. Внутри каждой зоны страничные кадры обрабаты-
обрабатываются компонентом, называемым buddy-системой. В целях повышения про-
производительности небольшое количество страничных кадров содержится в
кэше, позволяющем быстро удовлетворить запросы на выделение единичных
страничных кадров.
Запрашивание и освобождение страничных кадров
Страничные кадры запрашиваются с помощью шести слегка различающихся
функций и макросов. Если не оговорено обратное, то они возвращают линей-
линейный адрес первой выделенной страницы или null в случае неудачи:
□ allocjpages (gfpmask, order) — макрос ИСПОЛЬЗуетСЯ ДЛЯ запроса СМеж-
ных страничных кадров, количество кадров равно двум в степени order.
Он возвращает адрес дескриптора первого выделенного страничного кадра
или null, если выделение завершилось неудачей;
□ allocjpage(gfp_mask) — макрос ИСПОЛЬЗуетСЯ ДЛЯ получения ОДНОГО СТра-
ничного кадра. Он расширяется в вызов:
alloc_pages(gfp_mask, 0)
Возвращает адрес дескриптора первого выделенного страничного кадра
или null, если выделение завершилось неудачей;
□ get_free_pages (gfpjmask, order) — функция аналогична макросу alloc_
pages (), но возвращает линейный адрес первой выделенной страницы;
□ get_free_page(gfp_mask) — макрос ИСПОЛЬЗуетСЯ ДЛЯ получения ОДНОГО
страничного кадра. Он расширяется в вызов:
get_free_pages(gfp_mask, 0)
□ get_zeroed_page(gfp_mask) — функция ИСПОЛЬЗуетСЯ ДЛЯ получения СТра-
ничного кадра, заполненного нулями. Она делает следующий вызов:
alloc_pages(gfp_mask | GFP_ZERO, 0)
и возвращает линейный адрес выделенного страничного кадра;
□ get_dma_pages(gfp_mask, order) — макрос ИСПОЛЬЗуетСЯ ДЛЯ Получения
страничных кадров, подходящих для DMA. Он расширяется в вызов:
get_free_pages(gfp_mask | GFP_DMA, order)
Параметр gfpmask представляет группу флагов, определяющих способ
поиска свободных страничных кадров. Флаги, которые можно использо-
использовать в параметре gfpmask, перечислены в табл. 8.5.
Таблица 8.5. Флаги, используемые в запросе на страничные кадры
Флаг Описание
gfp_dma Страничный кадр должен принадлежать зоне zonejdma. Флаг экви-
эквивалентен флагу GFPJDMA
gfp_highmem Страничный кадр должен принадлежать зоне zone_highmem
Таблица 8.5 (окончание)
Флаг Описание
gfp_wait Ядру разрешается блокировать текущий процесс в ожидании сво-
свободных страничных кадров
gfphigh Ядру разрешается обращаться к пулу зарезервированных странич-
страничных кадров
gfp_io Ядру разрешается выполнять ввод/вывод в ситуации дефицита па-
памяти, чтобы освободить страничные кадры
gfp_fs Когда флаг сброшен, ядру не разрешено выполнять операции, зави-
зависящие от файловой системы
gfp_cold Запрошенные страничные кадры могут быть "холодными" (см.
разд. "Кэш страничных кадров процессора" далее в этой главе)
gfp_nowarn Неуспешное выделение памяти не приведет к выдаче предупрежде-
предупреждения
gfp_repeat Ядро повторяет попытки выделения памяти, пока не добьется успеха
GFP_NOFAIL ТО Же, ЧТО GFPJREPEAT
gfp_noretry He повторять попытку в случае неуспешного выделения памяти
gfp_no_grow Slab-аллокатор не разрешает увеличивать slab-кэш (см. разд. "Slab-
аллокатор" далее в этой главе)
gfp_comp Страничный кадр принадлежит расширенной странице
gfp_zero Возвращаемый страничный кадр (если таковой имеется) должен
быть заполнен нулями
На практике в операционной системе Linux применяются комбинации фла-
флагов, приведенные в табл. 8.6. Имя группы— это аргумент, передаваемый
функциям выделения страниц, описанным ранее в этом разделе.
Таблица 8.6. Группы флагов, используемые при запросе страничных кадров
Имя группы Соответствующие флаги
GFP_ATOMIC GFP_HIGH
GFP_NOIO GFP_WAIT
GFP_NOFS GFP_WAIT | GFP_IO
GFP_KERNEL GFPJWAIT | GFP_IO I GFP_FS
GFPJJSER GFP_WAIT | GFP_IO | GFP__FS
GFP__HIGHUSER GFP_WAIT | GFP_IO | GFP_FS I GFP_HIGHMEM
Флаги gfpdma и gfphighmem называются модификаторами зоны. Они
определяют, в какой зоне ядро ищет свободные страничные кадры. Поле
nodezonelists дескриптора узла contigpagedata ЯВЛЯетСЯ маССИВОМ СПИ-
СПИСКОВ, включающих в себя дескрипторы зон, представляющие зоны отступле-
отступления. Для любого значения модификаторов зон соответствующий список со-
содержит зоны, которые можно использовать для удовлетворения запроса на
память, если в первоначально указанной зоне недостаточно страничных кад-
кадров. В архитектуре 80x86 с поддержкой UMA приняты следующие зоны от-
отступления:
□ если флаг gfpdma установлен, страничные кадры можно брать только из
зоны zone_dma;
□ в противном случае, и если не установлен флаг ^gfphighmem, страничные
кадры можно брать только из зон zonenormal и zonedma, указанных здесь
в порядке предпочтения;
□ в противном случае (флаг ^gfphighmem установлен), страничные кадры
можно брать из зон zone_highmem, zone_normal и zone_dma, в порядке пред-
предпочтения.
Страничные кадры можно освобождать с помощью следующих функций и
макросов:
□ f reepages (page, order) — эта функция проверяет дескриптор СТранИ-
цы, на который указывает параметр page. Если данный страничный кадр
не зарезервирован (то есть флаг PGreserved равен 0), функция уменьшает
поле count дескриптора. Если значение count стало равно 0, функция счи-
считает, что смежные страничные кадры (количестве кадров равно двум в
степени order), начиная с того, на который указывает параметр page,
больше не используются. В этом случае функция освобождает их, как
описано ъразд. "Аллокатор зон" далее в этой главе\
П f reepages (addr, order) — Эта фуНКЦИЯ аналогична функции
f reepages (), но в качестве первого параметра она принимает линейный
адрес addr первого страничного кадра, подлежащего освобождению;
□ freepage(page) — этот макрос освобождает страничный кадр, на кото-
который указывает параметр page. Макрос расширяется В f reepages (page, 0);
П f reepage (addr) — этот макрос освобождает страничный кадр с линейным
адресом addr. Макрос расширяется В free_pages (addr, 0).
Отображение ядром страничных кадров
верхней памяти
Линейный адрес, соответствующий концу напрямую отображенной физиче-
физической памяти и, следовательно, началу верхней памяти, хранится в перемен-
ной highmemory, значение которой равно 896 Мбайт. Страничные кадры вы-
выше 896-мегабайтной границы, вообще говоря, не отображаются в четвертый
гигабайт линейного адресного пространства ядра, и оно не может обращаться
к ним непосредственно. Это означает, что никакая функция аллокатора стра-
страниц, возвращающая линейный адрес выделенного страничного кадра, не ра-
работает со страничными кадрами из верхней памяти, т. е. из зоны
ZONE_HIGHMEM.
Предположим, например, что ядро вызвало функцию get_free_
pages (gfp_highmem, 0), чтобы выделить страничный кадр в верхней памяти.
Если аллокатор выделяет страничный кадр в верхней памяти, функция
getfreepages о не может возвратить его линейный адрес, поскольку он не
существует. Поэтому функция возвращает null. Ядро не может использовать
выделенный страничный кадр, и, что еще хуже, он не может быть освобож-
освобожден, потому что ядро потеряло его след.
Эта проблема не возникает на 64-разрядных платформах, поскольку на них
пространство линейных адресов превышает объем устанавливаемой опера-
оперативной памяти. Короче говоря, в таких архитектурах зона zonehighmem всегда
пуста. На 32-разрядных платформах, таких как 80x86, разработчикам опера-
операционной системы Linux пришлось искать способ, позволяющий ядру экс-
эксплуатировать всю доступную оперативную память, вплоть до 64 Гбайт, под-
поддерживаемую механизмами РАЕ. Принятый подход заключается в следую-
следующем:
□ выделение страничных кадров в верхней памяти происходит только с по-
помощью функции aiiocpages о и ее специализированного варианта
aiioc_page(). Эти функции не возвращают линейный адрес первого выде-
выделенного страничного кадра, потому что в случае принадлежности стра-
страничного кадра верхней памяти такой линейный адрес просто не существу-
существует. Зато функции возвращают линейный адрес дескриптора первого выде-
выделенного страничного кадра. Эти линейные адреса существуют в любом
случае, поскольку все дескрипторы страниц выделяются раз и навсегда в
нижней памяти на этапе инициализации ядра;
П1 страничные кадры в верхней памяти, не имеющие линейного адреса, не-
недоступны ядру. Поэтому часть последних 128 Мбайт пространства линей-
линейных адресов ядра предназначена для отображения страничных кадров
верхней памяти. Конечно, это временное отображение; в противном слу-
случае были бы доступны только 128 Мбайт верхней памяти. Благодаря цик-
циклическому характеру отображения доступна вся оперативная память, хотя
и не сразу.
Для отображения страничных кадров верхней памяти ядро применяет три
различных механизма: постоянное отображение, временное отображение и
выделение несмежной памяти. В этом разделе мы обсуждаем первые две тех-
техники; третья описана в разд. "Управление несмежными областями памяти"
далее в этой главе.
Создание ядром постоянного отображения может блокировать текущий про-
процесс. Это происходит при отсутствии свободных записей в Таблице Страниц,
которые могут быть использованы в качестве "окон" для страничных кадров
в верхней памяти. Следовательно, ядро не может создать постоянное отобра-
отображение в обработчиках прерываний и в функциях отложенного выполнения.
С другой стороны, установка временного отображения ядром никогда не при-
приведет к блокированию текущего процесса. Недостатком такого подхода
является очень небольшое количество временных отображений, устанавли-
устанавливаемых одновременно.
Управляющий тракт ядра, использующий временное отображение, должен
обеспечить невозможность использования того же отображения другим пото-
потоком. Иными словами, управляющий тракт ядра нельзя блокировать, т. к. в
противном случае другой управляющий тракт сможет воспользоваться тем
же окном для отображения другой страницы верхней памяти.
Конечно, ни одна из этих техник не позволяет сразу обратиться ко всей опе-
оперативной памяти. В конце концов, для отображения верхней памяти остается
128 Мбайт пространства линейных адресов, в то время как механизм РАЕ
поддерживает системы, имеющие до 64 Гбайт оперативной памяти.
Постоянные отображения
Постоянные отображения позволяют ядру установить долговременное ото-
отображение страничных кадров верхней памяти в адресное пространство ядра.
Они используют специально отведенную для этих целей Таблицу Страниц из
числа главных таблиц страниц ядра. Переменная pkmappage table хранит
адрес этой Таблицы Страниц, а макрос lastpkmap возвращает количество
записей. Как обычно, Таблица Страниц включает в себя либо 512, либо
1024 записей, в зависимости от того, задействован ли механизм РАЕ. В ре-
результате ядро может одновременно адресовать максимум два или четыре ме-
мегабайта верхней памяти.
Таблица Страниц отображает линейные адреса, начиная с pkmapbase. Массив
pkmapcount содержит lastpkmap счетчиков, по одному на каждую запись
Таблицы Страниц pkmappagetabie. Мы различаем три случая:
□ счетчик равен 0 — соответствующая запись Таблицы Страниц не отобра-
отображает никакой страничный кадр верхней памяти и может быть использо-
использована;
□ счетчик равен 1 — соответствующая запись Таблицы Страниц не отобра-
отображает никакой страничный кадр верхней памяти, но использовать ее нель-
зя, потому что соответствующая запись TLB-буфера не была очищена с
момента последнего использования;
□ счетчик равен п (больше 1) — соответствующая запись Таблицы Страниц
отображает страничный кадр верхней памяти, которым пользуются п-\
компонентов ядра.
Для отслеживания связи между страничными кадрами верхней памяти и ли-
линейными адресами, установленной постоянным отображением, ядро пользу-
пользуется хеш-таблицей pageaddresshtabie. Эта таблица содержит по одной
структуре pageaddressmap для каждого страничного кадра верхней памяти,
отображенного в данный момент. В свою очередь, каждая структура содер-
содержит указатель на дескриптор страницы и линейный адрес, присвоенный стра-
страничному кадру.
Функция pageaddressO возвращает линейный адрес, ассоциированный со
страничным кадром, или null, если страничный кадр находится в верхней
памяти, но не отображен. Эта функция, принимающая в качестве параметра
указатель на дескриптор страницы page, различает два случая:
□ Если страничный кадр не расположен в верхней памяти (флаг PGhighmem
сброшен), линейный адрес существует всегда и получается в результате
вычисления индекса страничного кадра, преобразования его в физический
адрес и вычисления линейного адреса, соответствующего физическому.
Все это выполняется в следующем фрагменте кода:
va ( (unsigned long) (page — inem_map) « 12)
□ Если страничный кадр находится в верхней памяти (флаг PGhighmem уста-
установлен), функция рассматривает хеш-таблицу pageaddresshtabie. Если
страничный кадр обнаруживается в хеш-таблице, функция pageaddressO
возвращает его линейный адрес; в противном случае она возвращает null.
Функция kmapo устанавливает постоянное отображение. Она эквивалентна
следующему коду:
void * kmap(struct page * page)
{
if (!PageHighMem(page))
return page_address(page);
return kmap_high(page);
}
Функция kmaphigh () вызывается, если страничный кадр действительно при-
принадлежит верхней памяти. Она эквивалентна следующему коду:
void * kmap_high(struct page * page)
{
unsigned long vaddr;
spin_lock(&kmap_lock);
vaddr = (unsigned long) page_address(page);
if (!vaddr)
vaddr = roap_new_virtual(page);
pkmap_count[ (vaddr-PKMAP_BASE) » PAGE_SHIFT]++;
spin_unlock(&kmap_lock);
return (void *) vaddr;
}
Эта функция получает спин-блокировку kmapiock для защиты Таблицы
Страниц от попыток одновременного обращения в многопроцессорных сис-
системах. Обратите внимание на отсутствие необходимости отключать прерыва-
прерывания, поскольку функция kmap () не может быть вызвана обработчиком преры-
прерывания или функцией отложенного выполнения. Функция kmaphigho затем
проверяет, отображен ли уже страничный кадр, для чего вызывает функцию
page_address(). Если ответ отрицательный, функция вызывает функцию
mapnewvirtual(), чтобы занести физический адрес страничного кадра
в запись таблицы pkmappagetabie и добавить элемент в хеш-таблицу
pageaddresshtable. После ЭТОГО функция kmap_high() увеличивает счетчик,
соответствующий линейному адресу страничного кадра, чтобы учесть новый
компонент ядра, вызвавший эту функцию. Наконец, функция kmaphigh () ос-
освобождает спин-блокировку kmapiock и возвращает линейный адрес, ото-
отображающий страничный кадр.
Функция mapnewvirtual () выполняет два цикла, один внутри другого:
for (;;) {
int count;
DECLARE JfAITQUEUE(wait, current);
for (count = LASTJPKMAP; count > 0; —count) {
last_pkmap_nr = (last_pkmap_nr + 1) & (LAST_PKMAP - 1) ;
if (!last_pkmap_nr) {
f lush_all_zero_pkmaps () ;
count = LAST_PKMAP;
}
if (!pkmap_count [last_pkmap_nr] ) {
unsigned long vaddr = PKMAP_BASE +
(last_pkmap_nr « PAGE_SHIFT) ;
set_pte (& (pkmap_page_table [last_pkmap_nr] ) ,
mk_pte (page, pgprot @x63) )) ;
pkmap_count [last_pkmap_nr] =1;
set_page_address(page, (void *) vaddr);
return vaddr;
}
}
current->state = TASKJJNINTERRUPTIBLE;
add_wai t_queue (&pkmap_rnap_wa it, &wa i t);
spin_unlock (&kmap_lock) ;
schedule();
remove_wait_queue (&pkmap_map_wait, &wait) ;
spin lock(&kmap lock);
if (page_address(page))
return (unsigned long) page_address(page);
}
Во внутреннем цикле функция перебирает все счетчики в массиве
pkmapcount, пока не найдет счетчик с нулевым значением. Большой блок if
выполняется, когда в массиве pkmapcount обнаруживается неиспользуемый
элемент. Этот блок определяет линейный адрес, соответствующий данному
элементу, создает для него запись в Таблице Страниц pkmappagetabie, уста-
устанавливает счетчик в 1, поскольку теперь запись используется, вызывает
функцию setpageaddress о, чтобы занести новый элемент в хеш-таблицу
pageaddresshtabie, и возвращает линейный адрес.
Функция начинает поиск с того места, где закончила последний раз, переби-
перебирая элементы массива pkmapcount. Она может это сделать, потому что сохра-
сохранила в переменной lastpkmapnr индекс последней использованной записи в
Таблице Страниц pkmappagetabie. Итак, поиск начинается с того места, где
ОН был Закончен При Последнем ВЫЗОВе фуНКЦИИ map_new_virtual ().
Когда достигнут последний элемент массива pkmapcount, поиск возобновля-
возобновляется со счетчика с индексом 0. Однако прежде чем продолжить, функция
map_new_virtual () вызывает функцию f lush_all_zero_pkmaps (), КОТОрая на-
чинает свой перебор счетчиков в поисках тех, значение которых равно еди-
единице. Каждый счетчик с единичным значением соответствует записи в таб-
таблице pkmappagetabie, которая свободна, но не может быть использована из-
за того, что связанный с ней элемент TLB-буфера еще не был очищен. Функ-
Функция fiushaiizeropkmaps о записывает нули в эти счетчики, удаляет соот-
соответствующие элементы ИЗ хеш-таблицы pageaddresshtabie И ВЫПОЛНЯет
сброс элементов TLB-буфера на диск для всех записей таблицы
р kmap_pa ge_t ab 1 е.
Если по ходу внутреннего цикла счетчик с нулевым значением не был найден
в массиве pkmapcount, функция mapnewvirtuaio блокирует текущий про-
процесс, пока какой-нибудь другой процесс не освободит запись в Таблице
Страниц pkmappagetabie. Это достигается путем постановки процесса
current В Очередь pkmapmapwait, установки его В СОСТОЯНИе TASK_
UNINTERRUPTIBLE И ВЫЗОВе фуНКЦИИ schedule (), Чтобы уСТуПИТЬ Процессор
другим задачам. После пробуждения процесса функция проверяет, отобразил
ЛИ ДРУГОЙ процесс Страницу, ДЛЯ Чего ВЫЗЫВаеТ фуНКЦИЮ page address () . ЕС-
ЕСЛИ никакой процесс еще не отобразил эту страницу, возобновляется внутрен-
внутренний цикл.
Функция kunmapo отменяет постоянное отображение, ранее установленное
функцией kmap (). Если страница действительно находится в зоне верхней па-
памяти, функция вызывает функцию kunmaphigh (), которая эквивалентна сле-
следующему коду:
void kunmap high(struct page * page)
{
spin lock(&kmap_lock);
if ((—pkmap_count[((unsigned long)page_address(page)
-PKMAP_BASE)»PAGE_SHIFT] ) == 1)
if (waitqueue_active (&pkmap_map_wait) )
wake up(&pkmap шар wait);
spin_unlock (&kmap__lock) ;
}
Выражение в скобках вычисляет индекс элемента в массиве pkmapcount по
линейному адресу страницы. Счетчик уменьшается и сравнивается с 1. Ра-
Равенство означает, что ни один процесс не использует страницу. Функция мо-
может разбудить процессы в очереди, которую заполнила функция
map_new_virtuai (), если таковые имеются.
Временные отображения
Реализовать временные отображения проще, чем постоянные. Кроме того,
они могут быть использованы в обработчиках прерываний и функциях отло-
отложенного выполнения, потому что запрос на временное отображение никогда
не блокирует текущий процесс.
Каждый страничный кадр в верхней памяти может быть отображен через ок-
окно в адресном пространстве ядра, а именно запись Таблицы Страниц, заре-
зарезервированную для этой цели. Количество окон, оставленных для временных
отображений, весьма невелико.
У каждого процессора имеется собственный набор из 13 окон, представлен-
представленный структурой enum km type. Каждый символ, определенный в этой структу-
структуре (например, km_bounce_read, km_usero или км_ртео), идентифицирует линей-
линейный адрес окна.
Ядро должно обеспечить невозможность использования одного окна двумя
управляющими трактами одновременно. Поэтому каждый символ в структу-
структуре kmtype ассоциирован с одним компонентом ядра и назван по имени этого
компонента. Последний символ, kmtypenr, не представляет никакой линей-
ный адрес, а возвращает количество разных окон, используемых каждым
процессором.
Любой символ в структуре kmtype, кроме последнего, является индексом
фиксированно отображенного линейного адреса (см. главу 2). Структура enum
fixed_addresses ВКЛЮЧает В Себя СИМВОЛЫ FIX_KMAP_BEGIN И FIX_KMAP_END,
причем последний присвоен индексу fix_kmap_begin + (km_type_nr *
nrcpus) - 1. Таким образом, для каждого процессора в системе имеется
kmtypenr фиксировано отображенных линейных адресов. Кроме того, ядро
инициализирует переменную kmappte адресом записи Таблицы Страниц, со-
соответствующей линейному адресу f ix_to_virt (fix_kmap_begin) .
Чтобы установить временное отображение, ядро вызывает функцию
kmapatomic (), которая эквивалентна следующему коду:
void * kmap_atomic (struct page * page, enum km_type type)
{
enum fixed_addresses idx;
unsigned long vaddr;
current_thread_info()->preempt_count++;
if (!PageHighMem(page))
return page_address(page);
idx = type + KM_TYPE_NR * smp_processor_id() ;
vaddr = fix_to_virt(FIX_KMAP_BEGIN + idx) ;
set_pte(kmap_pte-idx, mk_pte(page, 0x063));
flush_tlb_single(vaddr);
return (void *) vaddr;
}
Аргумент type и идентификатор процессора, полученный с помощью функ-
функции smpprocessorido, определяют, какой фиксированно отображенный ад-
адрес должен быть использован для отображения запрошенной страницы.
Функция возвращает линейный адрес страничного кадра, если он не принад-
принадлежит верхней памяти; в противном случае она заносит в запись Таблицы
Страниц, соответствующую фиксированно отображенному линейному адре-
адресу, физический адрес страницы И флаги Present, Accessed, Read/Write И Dirty.
В завершение своей работы функция очищает соответствующий элемент
TLB-буфера и возвращает линейный адрес.
Для отмены временного отображения ядро вызывает функцию kunmap_
atomic о. В архитектуре 80x86 эта функция уменьшает счетчик preemptcount
у текущего процесса. Таким образом, если поток ядра был вытесняемым
непосредственно перед запросом временного отображения, он будет вы-
вытесняемым и после отмены этого отображения. Кроме того, функция
kunmap_atornic () проверяет, установлен ли флаг tif_need_resched у процесса
current, и, если установлен, вызывает функцию schedule ().
Алгоритм "buddy-система"
При выделении групп смежных страничных кадров ядро должно придержи-
придерживаться устойчивой и эффективной стратегии. По ходу дела оно сталкивается с
хорошо известной проблемой, связанной с управлением памятью и называе-
называемой внешней фрагментацией. Суть ее в том, что частые запросы на выделе-
выделение и освобождение групп смежных страничных кадров разного размера мо-
могут привести к ситуации, в которой несколько небольших блоков свободных
страничных кадров вкраплены внутрь блоков выделенных страничных кад-
кадров. В результате может оказаться невозможным выделение большого блока
смежных страничных кадров, даже если имеется достаточно свободных стра-
страниц для удовлетворения запроса.
Существует два основных способа борьбы с внешней фрагментацией:
□ применение электронной схемы управления памятью для отображения
групп несмежных свободных страничных кадров в непрерывные интерва-
интервалы линейных адресов;
□ разработка подходящей техники отслеживания существующих блоков из
свободных смежных страничных кадров, которая позволяла бы, по воз-
возможности, избегать разбиения большого свободного блока ради удовле-
удовлетворения запроса на меньший блок.
Второй подход является более предпочтительным для ядра по трем веским
причинам:
□ в некоторых случаях смежные страничные кадры действительно необхо-
необходимы, потому что недостаточно иметь смежные линейные адреса для
удовлетворения запроса. Типичным примером является запрос на память
для буферов, назначаемых процессору DMA (см. главу 13). Поскольку
большинство таких процессоров игнорирует электронную схему управле-
управления страницами и напрямую обращается к адресной шине при передаче
нескольких секторов диска за одну операцию ввода/вывода, запрошенные
буферы должны находиться в смежных страничных кадрах;
□ даже если выделение смежных страничных кадров не является строго не-
необходимым, оно имеет то достоинство, что оставляет неизменными табли-
таблицы страницы ядра. Почему их нежелательно изменять? Как мы знаем из
главы 2, частые изменения Таблицы Страниц увеличивают среднее время
обращения к памяти, потому что из-за них процессору приходится очи-
очищать содержимое TLB-буферов;
□ ядро может обращаться к большим непрерывным фрагментам физической
памяти, пользуясь 4-мегабайтными страницами. Это сокращает промахи
мимо TLB-буфера, чем значительно уменьшает среднее время обращения
к памяти.
Подход к решению проблемы внешней фрагментации, принятый в Linux, ос-
основан на хорошо известном алгоритме buddy-системы. Все свободные стра-
страничные кадры собираются в 11 списков, содержащих блоки, состоящие, соот-
соответственно, из 1,2, 4, 8, 16, 32, 64, 128, 256, 512 и 1024 смежных страничных
кадров. Самый "крупный" запрос на 1024-страничных кадра соответствует
непрерывному фрагменту памяти размером 4 Мбайт. Физический адрес пер-
первого страничного кадра в блоке кратен размеру группы. Например, началь-
начальный адрес блока из 16-страничных кадров кратен 16 х 212 B12 = 4096 является
обычным размером страницы).
Мы объясним работу алгоритма на простом примере.
Предположим, что выдан запрос на группу из 256 смежных страничных кад-
кадров (то есть на один мегабайт памяти). Вначале алгоритм проверяет наличие
свободного блока в списке блоков из 256-страничных кадров. Если такого
блока нет, алгоритм ищет более крупный, 512-кадровый блок в соответст-
соответствующем списке. Если такой блок найден, ядро выделяет 256 из 512-стра-
ничных кадров для удовлетворения запроса и переносит оставшиеся
256-страничных кадров в список, содержащий 256-кадровые блоки. Если же
свободного блока из 512-страничных кадров тоже нет, ядро ищет следующий
более крупный блок (из 1024 кадров). Если его найти удалось, ядро выделяет
256 из 1024-страничных кадров для удовлетворения запроса, переносит пер-
первые 512 из оставшихся 768-страничных кадров в список 512-кадровых бло-
блоков, а остальные 256 кадров — в список 256-кадровых блоков. Если список
1024-кадровых блоков оказался пустым, алгоритм завершается и сигнализи-
сигнализирует об ошибке.
Обратная операция, освобождение блоков страничных кадров, дала название
этому алгоритму. Ядро пытается слить пары buddy-блоков размера Ъ в один
блок размера 2Ь. Два блока считаются buddy-блоками (от англ. "buddy" —
приятель), если:
□ оба блока имеют одинаковый размер Ъ\
□ они являются смежными физически;
□ физический адрес первого страничного кадра в первом блоке кратен
2 х Ъ х 212.
Алгоритм работает итеративно. Если ему удается слить освобожденные бло-
блоки, он удваивает Ъ и пытается создать блоки большего размера.
Структуры данных
Linux 2.6 пользуется отдельной buddy-системой для каждой зоны. Таким об-
образом, в архитектуре 80^86 имеется три buddy-системы. Первая обрабатывает
страничные кадры, подходящие для DMA-устройств, использующих шину
ISA, вторая — "нормальные" страничные кадры, а третья — страничные кад-
кадры из верхней памяти. Каждая buddy-система опирается в своей работе на
следующие структуры данных:
П массив memmap, обсуждавшийся ранее. Фактически, каждая зона имеет де-
дело с подмножеством элементов массива memmap. Первый элемент подмно-
подмножества и количество элементов в подмножестве задаются, соответственно,
ПОЛЯМИ zonememmap И size ДеСКрИПТОра ЗОНЫ;
□ массив, состоящий из 11 элементов типа f reearea, по одному для каждого
размера группы. Этот массив хранится в поле f reearea дескриптора зоны.
Рассмотрим к-и элемент массива freearea в дескрипторе зоны, который
идентифицирует свободные блоки размера 2к. Поле f reeiist этого элемента
является головой двунаправленного циклического списка, в котором собраны
дескрипторы страниц, ассоциированные со свободными блоками из
2к страниц. Точнее говоря, этот список включает в себя дескрипторы страниц,
описывающие начальные страничные кадры каждого блока из 2 свободных
страничных кадров. Указатели на соседние элементы списка хранятся в поле
lru дескриптора страницы5.
Кроме головы списка, к-и элемент массива f reearea содержит поле nrf гее,
которое задает количество свободных блоков размером в 2к страниц. Естест-
Естественно, если блоков по 2к свободных страничных кадров не существует, поле
nrf гее равно 0, а список пуст (оба указателя в поле f reeiist указывают на
само это поле).
Наконец, поле private дескриптора первой страницы блока 2к свободных
страниц хранит порядок блока, т. е. число к. Благодаря этому полю, когда
блок страниц освобождается, ядро может определить, свободен ли buddy-
блок этого блока, и, если это так, слить оба блока в один, состоящий из
2k+l страниц. Следует заметить, что вплоть до версии Linux 2.6 ядро работало
с десятью массивами флагов для кодирования этой информации.
Выделение блока
Функция rmqueue () служит для поиска свободного блока в зоне. Она при-
принимает два аргумента: адрес дескриптора зоны и order, двоичный логарифм
5 Как мы увидим впоследствии, поле lru дескриптора страницы может иметь другой смысл, когда
страница не свободна.
размера запрошенного блока свободных страниц @ для блока из одной стра-
страницы, 1 для двухстраничного блока и т. д.). Если выделение страничных кад-
кадров прошло удачно, функция rmqueueo возвращает адрес дескриптора
страницы, соответствующего первому выделенному страничному кадру.
В противном случае функция возвращает null.
Функция rmqueueo предполагает, что возвращающая ее функция уже от-
отключила локальные прерывания и получила спин-блокировку zone->iock, за-
защищающую структуры buddy-системы. Функция в цикле перебирает элемен-
элементы каждого списка в поисках свободного блока (то есть элемента, который бы
не указывал сам на себя), начиная со списка, имеющего порядок order, и пе-
переходя к спискам большего порядка в случае необходимости:
struct free_area *area;
unsigned int current__order;
for (current_order=order; current_order<ll; ++current_order) {
area = zone->free_area + current_order;
if (!list_empty(&area->free_list))
goto block_found;
}
return NULL;
Если цикл завершается естественно, значит, свободный блок не был найден, и
функция rmqueue () возвращает значение null. В противном случае был
найден подходящий свободный блок, и тогда дескриптор его первого стра-
страничного кадра удаляется из списка, а значение поля f reepages дескриптора
зоны уменьшается:
block_found:
page = list_entry(area->free_list.next, struct page, lru);
list_del(&page->lru);
ClearPagePrivate(page);
page->private = 0;
area->nr_free—;
zone->free_pages -= 1UL « order;
ЕСЛИ НаЙДеННЫЙ 6ЛОК СОДерЖИТСЯ В СПИСКе, ПОрЯДОК КОТОРОГО curr_order
больше запрошенного порядка order, выполняется цикл while. Смысл этих
строчек кода таков: когда необходимо воспользоваться блоком из 2к странич-
страничных кадров для удовлетворения запроса на 2й страничных кадров (h < к), про-
программа выделяет первые 2h страничных кадров и итеративно переносит ос-
оставшиеся 2к — 2й страничных кадров в списки f reearea, имеющие индексы
между h и к:
size = 1 « curr_order;
while (curr_order > order) {
area—;
curr_order—;
size »= 1;
buddy = page + size;
/* insert buddy as first element in the list */
list_add(&buddy->lru, &area->free_list);
area->nr_free++;
buddy->private = curr_order;
SetPagePrivate(buddy);
}
return page;
Найдя подходящий свободный блок, функция rmqueue () возвращает адрес
page дескриптора страницы, ассоциированного с первым выделенным стра-
страничным кадром.
Освобождение блока
Функция freepagesbuiko реализует стратегию buddy-системы для осво-
освобождения страничных кадров. Она принимает три входных параметра6:
□ page— адрес дескриптора первого страничного кадра в освобождаемом
блоке;
□ zone — адрес дескриптора зоны;
□ order — логарифмический размер блока.
Функция предполагает, что возвращающая ее функция уже отключила ло-
локальные прерывания и получила спин-блокировку zone->iock, защищающую
структуры buddy-системы. Функция freepagesbuiko начинает работу с
объявления и инициализации нескольких локальных переменных:
struct page * base = zone->zone_mem_map;
unsigned long buddy_idx, page_idx = page — base;
struct page * buddy, * coalesced;
int order_size = 1 « order;
Локальная переменная pageidx содержит индекс первого страничного кадра
в блоке относительно первого страничного кадра зоны.
6 В целях повышения производительности эта встроенная функция принимает еще один параметр,
но его значение вычисляется по трем основным параметрам, обсуждаемым в тексте.
Локальная переменная ordersize служит для увеличения счетчика свобод-
свободных страничных кадров зоны:
zone->free_pages += order_size;
Затем функция выполняет цикл из максимум Ю-order операций для каждой
возможности слияния блока с его buddy-блоком. Функция начинает с блоков
наименьшего размера и двигается в направлении увеличения размера:
while (order < 10) {
buddy_idx = page_idx л A « order);
buddy = base + buddy_idx;
if (!page_is_buddy(buddy, order))
break;
list_del(&buddy->lru);
zone->free_area[order].nr free—;
ClearPagePrivate(buddy);
buddy->private = 0;
page_idx &= buddy_idx;
order++;
}
В теле цикла функция ищет индекс buddyidx блока, являющегося buddy-
блоком для блока, у которого индекс дескриптора страницы равен pageidx.
Оказывается, этот индекс легко вычислить следующим образом:
buddy_idx = page_idx A A « order);
Операция ИСКЛЮЧАЮЩЕЕ ИЛИ (XOR), применяющая маску (i«order)?
переключает значение order-го бита в индексе pageidx. Следовательно, если
бит был равен 0, ТО индекс buddyidx равен pageidx + ordersize. И наобо-
рОТ, еСЛИ бит был равен 1, индекс buddyidx равен pageidx - ordersize.
Узнав индекс buddy-блока, легко получить дескриптор страницы для этого
блока:
buddy = base + buddy_idx;
После этого функция вызывает функцию pageisbuddyO для проверки, дей-
действительно ли дескриптор buddy описывает первую страницу блока из
ordersize свободных страничных кадров:
int page_is_buddy(struct page *page, int order)
{
if (PagePrivate(buddy) && page->private == order &&
IPageReserved(buddy) && page_count(page) ==0)
return 1;
return 0;
}
Нетрудно видеть, что первая страница buddy-блока должна быть свободной
(поле count равно -1), она должна принадлежать динамической памяти (бит
PGreserved сброшен), ее поле private должно содержать осмысленную ин-
информацию (бит PGprivate установлен), и, наконец, в поле private должен
храниться порядок освобождаемого блока.
Если все эти условия удовлетворены, buddy-блок свободен, и функция удаля-
удаляет его из списка свободных блоков порядка order и выполняет одну или не-
несколько итераций в поисках вдвое больших buddy-блоков.
Если хотя бы одно из условий функции pageisbuddyo нарушено, описы-
описываемая функция прерывает цикл, поскольку полученный свободный блок не
может быть далее слит с другими свободными блоками. Функция заносит его
в соответствующий список и обновляет поле private первого страничного
кадра, записывая туда порядок размера блока:
coalesced = base + page_idx;
coalesced->private = order;
SetPagePrivate(coalesced);
list_add(&coalesced->lru, &zone->free_area[order].free_list);
zone->free_area[order].nr_free++;
Кэш страничных кадров процессора
Как мы увидим далее в этой главе, ядро часто запрашивает и освобождает
одиночные страничные кадры. Для повышения производительности в каждой
зоне памяти определен кэш страничных кадров для каждого процессора. Это
процессорный кэш содержит некоторое количество заранее выделенных
страничных кадров, используемых для одиночных запросов на память, выда-
выдаваемых локальным процессором.
На самом деле, для каждого процессора в каждой зоне памяти существует два
кэша: "горячий", со страницами, содержимое которых, весьма вероятно, бу-
будет занесено в аппаратный кэш процессора, и "холодный".
Получение страничного кадра из "горячего" кэша выгодно с точки зрения
производительности системы, если либо ядро, либо процесс режима пользо-
пользователя будут записывать информацию в этот кадр сразу после его выделения.
Каждое обращение к ячейке памяти в страничном кадре на практике превра-
превращается в строчку аппаратного кэша, "отобранную" у другого страничного
кадра, конечно, если аппаратный кэш еще не содержит строчку, отображаю-
отображающую ячейку "горячего" страничного кадра, к которому произошло обра-
обращение.
С другой стороны, получение страничного кадра из "холодного" кэша удоб-
удобно, если планируется заполнить страничный кадр операцией прямого доступа
к памяти. В этом случае процессор не задействуется, и ни одна строчка аппа-
аппаратного кэша не будет изменена. Получение страничного кадра из "холодно-
"холодного" кэша позволяет сберечь резерв "горячих" страничных кадров для других
видов запросов на выделение памяти.
Основной структурой, реализующей кэш страничных кадров процессора, яв-
является массив структур per_cpu_pageset, который Хранится В поле pageset
дескриптора зоны памяти. Массив содержит по одному элементу для каждого
процессора. Этот элемент состоит из двух дескрипторов percpupages, один
из которых предназначен для "горячего" кэша, а другой — для "холодного".
Поля дескриптора percpupages перечислены в табл. 8.7.
Таблица 8.7. Поля дескриптора per_cpujpages
Тип Имя Описание
int count Количество страничных кадров в кэше
int low Нижняя отметка для пополнения кэша
int high Верхняя отметка для чистки кэша
int batch Количество страничных кадров, добавляемых в кэш
или удаляемых из него
struct listhead list Список дескрипторов страничных кадров, находящихся
в кэше
Ядро ведет мониторинг размеров "горячего" и "холодного" кэшей при помо-
помощи двух отметок. Если количество страничных кадров в кэше опускается ни-
ниже отметки low, ядро пополняет кэш, выделяя batch одиночных страничных
кадров из buddy-системы. Если же количество кадров переходит через отмет-
отметку high, ядро освобождает batch страничных кадров кэша и возвращает их
buddy-системе. Значения batch, low и high зависят от количества страничных
кадров в зоне памяти.
Выделение страничных кадров с помощью кэшей
страничных кадров процессора
Функция buf f eredrmqueue () выделяет страничные кадры в заданной зоне па-
памяти. Она пользуется кэшами страничных кадров процессора при обработке
запросов на одиночные страничные кадры.
Параметрами функции являются адрес дескриптора зоны памяти, порядок
запроса на выделение памяти order и флаги выделения gfp_flags. Если в па-
параметре gfpf lags установлен флаг gfp_cold, значит, страничный кадр дол-
должен быть взят из "холодного" кэша; в противном случае его следует брать из
"горячего" (этот флаг имеет смысл только при запросах на одиночные стра-
страничные кадры). Функция выполняет следующие действия:
1. Если значение параметра order не равно 0, кэш страничных кадров про-
процессора не используется, и функция переходит к шагу 4.
2. Проверяет, нужно ли пополнить кэш локального процессора, расположен-
расположенный в данной зоне памяти и идентифицируемый значением флага
gfpcold (поле count дескриптора percpupages меньше или равно полю
low). В таком случае функция выполняет следующие действия:
• выделяет batch одиночных страничных кадров из buddy-системы, мно-
многократно вызывая функцию rmqueue ();
• заносит дескрипторы выделенных страничных кадров в список кэша;
• обновляет значение поля count, прибавляя к нему количество фактиче-
фактически выделенных страничных кадров.
3. Если значение count положительно, функция получает страничный кадр из
списка кэша, уменьшает count и переходит к шагу 5. (Заметьте, что кэш
страничных кадров процессора мог оказаться пустым; это случается, когда
функции rmqueue (), вызванной на шаге 2, не удается выделить ни одного
страничного кадра.)
4. Если функция выполняет этот шаг, значит, запрос еще не был удовлетво-
удовлетворен: либо потому, что для него требуется несколько смежных страничных
кадров, либо потому, что выбранный кэш страничных кадров пуст. Функ-
Функция вызывает функцию rmqueue о, чтобы выделить запрошенные стра-
страничные кадры в buddy-системе.
5. Если запрос на память удовлетворен, функция инициализирует дескриптор
страничного кадра (первого или единственного), а именно: сбрасывает не-
некоторые флаги, обнуляет поле private и записывает единицу в счетчик
ссылок страничного кадра. Кроме того, если флаг gpf_zero в поле
gfpfiags установлен, функция заполняет нулями выделенную область
памяти.
6. Возвращает адрес дескриптора, ассоциированного с (первым или единст-
единственным) страничным кадром, или null, если удовлетворить запрос на вы-
выделение памяти не удалось.
Освобождение страничных кадров
для кэшей страничных кадров процессора
Чтобы освободить одиночный страничный кадр для кэша страничных кадров
процессора, ядро вызывает функции free_hot_page() И f reecoldpage ().
Обе они являются всего лишь интерфейсными функциями для функции
f reehotcoidpage (), которая принимает в качестве параметров адрес page
дескриптора освобождаемого страничного кадра и флаг cold, определяющий
"горячий" или "холодный" кэш.
Функция f reehotcoidpage () выполняет следующие действия:
1. Извлекает из поля page->fiags адрес дескриптора зоны памяти, содержа-
содержащей данный страничный кадр.
2. Получает адрес дескриптора percpupages кэша, принадлежащего этой
зоне и определяемого флагом cold.
3. Проверяет, следует ли почистить кэш. Если значение count больше или
равно high, функция вызывает функцию freepagesbuiko передавая ей
дескриптор зоны, количество страничных кадров, подлежащих освобож-
освобождению (поле batch), адрес списка кэша и число ноль (для страничных кад-
кадров нулевого порядка). Что касается вызванной функции, она многократно
вызывает функцию f reepagesbulk (), чтобы освободить указанное ко-
количество страничных кадров (взятых из списка кэша) для buddy-системы
данной зоны.
4. Добавляет в список кэша освобождаемый страничный кадр и увеличивает
значение в поле count.
Следует заметить, что в текущей версии ядра Linux 2.6 страничные кадры ни
в какой ситуации не освобождаются для "холодного" кэша. Ядро всегда
предполагает, что освобождаемый страничный кадр является "горячим" по
отношению к аппаратному кэшу. Конечно, это не означает, что "холодный"
кэш пуст. Он пополняется функцией buf f eredrmqueue () по достижении ниж-
нижней отметки.
Аллокатор зон
Аллокатор зон является, образно говоря, передней частью аллокатора стра-
страничных кадров ядра. Этот компонент должен обнаружить зону памяти, со-
содержащую некоторое количество свободных страничных кадров, достаточное
для удовлетворения запроса на память. Эта задача не так проста, как может
показаться на первый взгляд, потому что перед аллокатором зон стоит ряд
целей:
□ защищать пул зарезервированных страничных кадров;
П запускать алгоритм утилизации страничных кадров (см. главу 17), когда
свободной памяти не хватает, а блокирование текущего процесса разре-
разрешено; после освобождения нескольких страничных кадров аллокатор зон
может повторить попытку выделения памяти;
□ по возможности, беречь небольшую и очень ценную зону памяти zonedma;
например, аллокатор зон не должен торопиться с выделением страничных
кадров в зоне zonedma, если поступил запрос на кадры в зоне zonenormal
ИЛИ ZONE_HIGHMEM.
Ранее мы видели, что любой запрос на группу смежных страничных кадров,
в конечном счете, обрабатывается с помощью макроса aiiocpages. Этот мак-
макрос, в свою очередь, вызывает функцию aiiocpages о, являющуюся "серд-
"сердцем" аллокатора зоны. Она принимает три параметра:
□ gfpmask — флаги, заданные в запросе на выделение памяти (см. табл. 8.5);
П order— логарифмический размер группы смежных страничных кадров,
подлежащих выделению;
□ zoneiist— указатель на структуру zoneiist, описывающую в порядке
предпочтения зоны, подходящие для выделения памяти.
Функция aiiocpageso просматривает каждую зону памяти, включенную
в структуру zoneiist. Код, выполняющий эту задачу, выглядит примерно так:
for (i = 0; (z=zonelist->zones[i]) != NULL; i++) {
if (zone_watermark_ok(z, order, ...)) {
page = buffered_rmqueue(z, order, gfp_mask);
if (page)
return page;
}
}
Для каждой зоны памяти функция сравнивает количество свободных стра-
страничных кадров с неким пороговым значением, которое зависит от флагов вы-
выделения памяти, от типа текущего процесса и от того, сколько раз функция
уже проверяла эту зону. На практике, если наблюдается дефицит свободной
памяти, каждая зона обычно сканируется несколько раз, причем с очередным
просмотром понижается порог минимального количества свободной памяти,
требуемой для выделения. Таким образом, приведенный фрагмент кода по-
повторяется несколько раз (с незначительными вариациями) в теле функции
aiiocpages (). Функция buf f eredrmqueue () уже была описана ранее в этой
главе: она возвращает дескриптор первого выделенного страничного кадра
или null, если зона памяти не содержит группу смежных страничных кадров
запрошенного размера.
Служебная функция zonewatermarkok () принимает несколько параметров,
которые определяют порог min для количества свободных страничных кадров
в зоне памяти. В частности, функция возвращает 1, если выполнены следую-
следующие два условия:
□ Кроме страничных кадров, которые необходимо выделить, в зоне памяти
имеется как минимум min свободных страничных кадров, на считая заре-
зервированных на случай дефицита памяти (поле lowmemreserve дескрип-
дескриптора зоны).
□ Кроме страничных кадров, которые необходимо выделить, имеется как
минимум m±rJ2k свободных страничных кадров в блоках порядка не мень-
меньше к (для каждого А: от 1 до порядка выделения). Следовательно, если
order больше нуля, должно быть, как минимум, min/2 свободных странич-
страничных кадров в блоках порядка не меньше 2; если order больше единицы,
должно быть как минимум min/4 свободных страничных кадров в блоках
порядка не меньше 4 и т. д.
Значение порога min определяется функцией zonewatermarkok () следующим
образом:
□ базовое значение передается в качестве параметра функции и может быть
равно ОДНОЙ ИЗ отметок В зоне: pages_min, pages_low ИЛИ pageshigh;
□I базовое значение делится пополам, если установлен флаг gfphigh, пере-
переданный в качестве параметра. Как правило, этот флаг установлен, если ус-
установлен флаг gfp_highmem в поле gfpmask, т. е. если страничные кадры
могут быть выделены в верхней памяти;
□ пороговое значение делится еще на четыре, если установлен флаг
cantryharder, передаваемый в качестве параметра. Этот флаг обычно ус-
установлен, либо если установлен флаг gfp_wait в поле gfpmask, либо ес-
если текущий процесс является процессом реального времени, и выделение
памяти происходит в контексте процесса (за пределами обработчиков пре-
прерываний и функций отложенного выполнения).
Функция aiioc_pages () выполняет следующие действия:
1. Производит первое сканирование зон памяти (фрагмент кода, приведен-
приведенный ранее). По ходу этого первого сканирования значение порога min ус-
устанавливается равным z->pages_iow, где z указывает на дескриптор рас-
рассматриваемой ЗОНЫ (параметры cantryharder И gfphigh равны 0).
2. Если функция не завершила работу на предыдущем шаге, значит, свобод-
свободной памяти осталось не очень много. Функция "будит" потоки ядра
kswapd, чтобы начать асинхронную утилизацию страничных кадров (см.
главу 17).
3. Выполняет второе сканирование зон памяти, передавая в качестве базово-
базового значения порога значение z->pages_min. Как было сказано ранее, окон-
окончательное значение порога определяется также флагами cantryharder и
gfphigh. Этот шаг практически идентичен шагу 1, но здесь функция рабо-
работает с меньшим порогом.
4. Если функция не завершила работу на предыдущем шаге, значит, в систе-
системе явный дефицит памяти. Если управляющий тракт, выдавший запрос на
выделение памяти, не является обработчиком прерываний и функцией
отложенного выполнения, и он пытается утилизировать страничные кад-
кадры (у текущего процесса установлен флаг pf_memalloc, либо pfmemdie),
функция выполняет третье сканирование зон памяти и пытается выделить
страничные кадры, игнорируя пороги дефицита памяти, т. е. — не вызы-
вызывая функцию zonewatermarkok (). Это единственный случай, КОГДа
управляющему тракту ядра разрешено исчерпать резерв страниц, опреде-
определяемый полем lowmemreserve дескриптора зоны. На самом деле, в этой
ситуации управляющий тракт ядра, сделавший запрос на память, в конеч-
конечном счете, пытается освободить некоторые страничные кадры, чтобы по-
получить запрошенную память, если это вообще возможно. Если ни в одной
зоне памяти нет достаточного количества страничных кадров, функция
возвращает null, чтобы известить вызывающую функцию о неудаче.
5. Если функция находится на этом шаге, значит, поток ядра не пытается
утилизировать память. Если флаг gfpwait в поле gfpmask не установ-
установлен, функция возвращает null, чтобы уведомить поток ядра о неудачной
попытке выделения памяти. В этой ситуации нет способа удовлетворить
запрос, не блокируя текущий процесс.
6. Если функция находится на этом шаге, значит, текущий процесс может
быть блокирован. Функция вызывает функцию condresched () для про-
проверки, не требуется ли процессор какому-нибудь другому процессу.
7. Устанавливает флаг pfmemalloc у процесса current, отмечая готовность
процесса выполнить утилизацию памяти.
8. Сохраняет в поле current->reciaim_state указатель на структуру
reclaimstate. Эта структура имеет ТОЛЬКО ОДНО поле, reclaimed_slab,
инициализированное нулем (применение этого поля обсуждается далее
в этой главе).
9. Вызывает функцию trytof reepages () для поиска страничных кадров,
которые можно утилизировать (см. разд. "Утилизация при дефиците па-
памяти" главы 17). Вызванная функция может блокировать текущий про-
процесс. Когда она возвращает управление, функция aiiocpageso сбра-
сбрасывает флаг pfmemalloc процесса current и снова вызывает функцию
cond_resched().
10. Если на предыдущем шаге удалось освободить некоторое количество
страничных кадров, функция выполняет еще одно сканирование зон па-
памяти, идентичное сканированию на шаге 3. Если запрос на выделение не
может быть удовлетворен, функция решает, следует ли ей продолжить
сканирование зоны памяти. Если флаг gfpnoretry сброшен, и либо за-
запрос на память охватывает до восьми страничных кадров, либо установ-
лен один из флагов gfprepeat или gfpnofail, функция вызывает
функцию bikcongestionwaito, чтобы ненадолго задержать процесс
(см. главу 14), и возвращается к шагу 6. В противном случае функция воз-
возвращает null, чтобы известить вызывающую функцию о неудаче.
11. Если на шаге 9 не был освобожден ни один страничный кадр, ядро стоит
перед серьезной проблемой, потому что возник опасный дефицит сво-
свободной памяти, а утилизировать страничные кадры не удалось. Настал
момент для принятия принципиально важного решения. Если потоку ядра
разрешено выполнять операции, зависящие от файловой системы и необ-
необходимые ДЛЯ уНИЧТОЖеНИЯ ПрОЦеССОВ (флаг GFPFS В ПОЛе gfp_mask
установлен), а флаг gfpnoretry сброшен, функция выполняет сле-
следующие действия:
• СНОВа Сканирует ЗОНЫ ПаМЯТИ С ПОРОГОВЫМ Значением z->pages_high;
• вызывает функцию outofmemory (), чтобы приступить к освобожде-
освобождению памяти путем уничтожения процесса (см. разд. "Уничтожение
процессов из-за нехватки памяти" главы 17);
• возвращается к шагу 1.
Поскольку отметка уровня, использованная на шаге 11, значительно выше
отметок, использованных при предыдущих сканированиях, весьма вероятно,
что этот шаг закончился неудачей. На практике шаг 11 завершается успешно,
только если другой поток ядра уже уничтожает процесс, чтобы получить его
память. Таким образом, шаг 11 позволяет избежать уничтожения двух ни
в чем не повинных процессов, вместо одного.
Освобождение группы страничных кадров
Аллокатор зон занимается также и освобождением страничных кадров.
К счастью, освобождение памяти намного проще ее выделения.
Все макросы и функции ядра, освобождающие страничные кадры (и описан-
описанные в разд. "Зонный аллокатор страничных кадров"ранее в этой главе), опи-
опираются в своей работе на функцию f reepages (). В качестве параметров
она принимает адрес дескриптора первого освобождаемого страничного кад-
кадра (page) и логарифмический размер группы смежных кадров, подлежащей
освобождению (order). Функция выполняет следующие действия:
1. Проверяет, действительно ли первый страничный кадр принадлежит ди-
динамической памяти (его флаг PGreserved должен быть сброшен). Если это
не так, завершает работу.
2. Уменьшает счетчик обращений page->_count. Если он все еще больше или
равен 0, завершает работу.
3. Если значение order равно 0, функция вызывает функцию f reehotpage (),
чтобы освободить страничный кадр для "горячего" кэша процессора в со-
соответствующей зоне памяти.
4. Если значение order больше 0, функция заносит страничные кадры в ло-
локальный список и вызывает функцию freepagesbuiko, чтобы освобо-
освободить их для buddy-системы соответствующей зоны (см. шаг 3 в описании
функции f reehotcoidpage () в разд. "Кэш страничных кадров процессо-
процессора" ранее в этой главе).
Управление областями памяти
В этом разделе речь пойдет об областях памяти, то есть о последовательно-
последовательностях ячеек, имеющих смежные физические адреса и произвольную длину.
Алгоритм "buddy-система" принимает страничный кадр в качестве базовой
области памяти. Такой подход хорош при обработке запросов на относитель-
относительно большие порции памяти, но что же делать с запросами на маленькие об-
области памяти из нескольких десятков или сотен байтов?
Очевидно, что было бы весьма расточительно выделять целый страничный
кадр для хранения нескольких байтов. Более удачное решение заключается во
введении новых структур, которые описывали бы, как небольшие области
памяти должны выделяться в пределах одного страничного кадра. Однако
при этом встает новая проблема, называемая внутренней фрагментацией.
Она возникает из-за несоответствия между размером запрашиваемой памяти
и выделенной для удовлетворения запроса.
Классический подход (принятый в ранних версиях Linux) состоит в представ-
представлении областей памяти с геометрически распределенными размерами. Иными
словами, размер области привязан к степени двойки, а не к размеру сохра-
сохраняемых данных. В этом случае независимо от размера запрашиваемой памяти
можно гарантировать, что внутренняя фрагментация будет всегда мень-
меньше 50%. Следуя такому подходу, ядро создает 13 геометрически распреде-
распределенных списков свободных областей памяти, размеры которых лежат в диа-
диапазоне от 32 до 131 072. Buddy-система используется как для получения до-
дополнительных страничных кадров, необходимых под новые области памяти,
так и для освобождения страничных кадров, более не содержащих никаких
областей. Для отслеживания свободных областей памяти в каждом странич-
страничном кадре применяется динамический список.
Slab-аллокатор
Выполнение алгоритма выделения области памяти на базе buddy-алгоритма
не особенно эффективно. Более удачный алгоритм основан на схеме slab-
аллокатора, которая впервые была задействована в операционной системе
Solaris 2.4 фирмы Sun Microsystems. Алгоритм исходит из следующих пред-
предположений:
□ Тип сохраняемых данных может влиять на способ выделения памяти; на-
например, при выделении страничного кадра процессу, работающему в ре-
режиме пользователя, ядро вызывает функцию getzeroedpage (), запол-
заполняющую страницу нулями. Концепция slab-аллокатора развивает эту идею
и рассматривает области памяти как объекты, состоящие из набора струк-
структур данных и пары функций или методов, называемых конструктором и
деструктором. Первый инициализирует область памяти, в то время как
второй деинициализирует ее.
Чтобы избежать повторной инициализации объектов, slab-аллокатор не
уничтожает объекты, которые были выделены, а потом освобождены.
Вместо этого он сохраняет их в памяти. Когда впоследствии запрашивает-
запрашивается новый объект, его можно взять из памяти, не инициализируя.
□ Функции ядра, как правило, многократно запрашивают области памяти
одного и того же типа. Например, когда ядро создает новый процесс, оно
выделяет области памяти под несколько таблиц фиксированного размера:
дескриптор процесса, объект "открытый файл" и т. д. (см. главу 3). После
того, как процесс закончится, области памяти, содержавшие эти таблицы,
можно будет использовать повторно. Поскольку процессы разрушаются и
уничтожаются довольно часто, без slab-аллокатора ядро непроизводитель-
непроизводительно расходовало бы время на многократное выделение и освобождение
страничных кадров, содержащих одни и же области памяти. Slab-
аллокатор позволяет сохранить их в кэше и использовать повторно.
□ Запросы на области памяти можно классифицировать по их частоте. За-
Запросы на определенный объект памяти, про которые известно, что они
возникают часто, можно очень эффективно обрабатывать, создав набор
специализированных объектов, имеющих соответствующий размер, и из-
избежав таким образом внутренней фрагментации. В то же время запросы на
"редкие" объемы памяти можно обрабатывать по схеме, основанной на
применении объектов с геометрически распределенными размерами (на-
(например, являющимися степенями двойки, как было принято в ранних вер-
версиях Linux), даже если этот подход приведет к внутренней фрагментации.
□ Использование объектов, размеры которых не являются геометрически
распределенными, имеет еще одно небольшое преимущество: начальные
адреса структур данных в меньшей степени концентрируются вокруг фи-
физических адресов, являющихся степенями двойки. Это, в свою очередь,
повышает эффективность аппаратного кэша процессора.
□ Забота об эффективности аппаратного кэша выдвигает дополнительный
аргумент в пользу минимизации вызовов аллокатора buddy-системы, на-
сколько это возможно. Каждый вызов функции buddy-системы "загрязня-
"загрязняет" аппаратный кэш, тем самым увеличивая среднее время доступа к памя-
памяти. Влияние функции ядра на аппаратный кэш называется ее следом и оп-
определяется, как процент объема кэша, переписываемый функцией, когда
заканчивается ее выполнение. Очевидно, что большие следы замедляют
выполнение кода, идущего непосредственно после функции, потому что
аппаратный кэш содержит бесполезную информацию.
Slab-аллокатор группирует объекты в кэши. Каждый кэш является хранили-
хранилищем объектов одного типа. Например, когда открывается файл, область памя-
памяти, необходимая для хранения объекта "открытый файл", берется из кэша
slab-аллокатора, называемого flip (от англ. "file pointer" —указатель на файл).
Область основной памяти, содержащая кэш, делится на участки. Каждый
участок состоит из одного или нескольких смежных страничных кадров, ко-
которые содержат как выделенные, так и свободные объекты (рис. 8.3).
Рис. 8.3. Компоненты slab-аллокатора
Как мы увидим в главе 17, ядро периодически сканирует кэши и освобождает
страничные кадры, соответствующие пустым участкам.
Дескриптор кэша
КаЖДЫЙ КЭШ ОПИСЫВаеТСЯ Структурой kmem_cache_t (ЧТО Эквивалентно struct
kmemcaches), поля которой перечислены в табл. 8.8. Мы опустили несколько
полей, используемых для сбора статистической информации и для отладки.
Таблица 8.8. Поля дескриптора kmem_cache_t
Тип Имя Описание
struct arraycache * [] array Массив указателей на локальные кэши
свободных объектов, имеющийся у каждого
процессора
Таблица 8.8 (продолжение)
Тип Имя Описание
unsigned int batchcount Количество объектов, передаваемых
пакетно локальным кэшам или получаемых
от них
unsigned int limit Максимальное количество свободных объ-
объектов в локальных кэшах. Может быть изме-
изменено
struct kmem_list3 lists См. табл. 8.9
unsigned int objsize Размер объектов, хранящихся в кэше
unsigned int flags Набор флагов, описывающих постоянные
свойства кэша
unsigned int num Количество объектов, упакованных в один
участок (все участки кэша имеют одинако-
одинаковый размер)
unsigned int free_limit Верхний предел количества свободных
объектов во всем slab-кэше
spinlock_t spinlock Спин-блокировка кэша
unsigned int gfporder Логарифм количества смежных страничных
кадров в отдельном slab-кэше
unsigned int gfpf lags Набор флагов, передаваемых функции
buddy-системы при выделении страничных
кадров
size_t colour Количество цветов для окрашивания участ-
участков памяти
unsigned int colour_of f Смещение базового выравнивания в участ-
участках памяти
unsigned int colour_next Цвет окрашивания следующего выделенно-
выделенного участка памяти
kmem_cache_t * slabp_cache Указатель на slab-кэш общего назначения,
содержащий дескрипторы участков памяти
(null, если используются внутренние деск-
дескрипторы участков памяти)
unsigned int slab_size Размер одного участка памяти
unsigned int df lags Набор флагов, определяющих динамиче-
динамические свойства кэша
void * ctor Указатель на метод-конструктор, ассоции-
ассоциированный с кэшем
void * dtor Указатель на метод-деструктор, ассоцииро-
ассоциированный с кэшем
const char * name Массив символов, содержащий имя кэша
Таблица 8.8 (окончание)
Тип Имя Описание
struct list_head next Указатели, используемые в двунаправлен-
двунаправленном списке дескрипторов кэша
Поле lists дескриптора kmemcachet само является структурой, поля кото-
которой перечислены в табл. 8.9.
Таблица 8.9. Поля структуры kmem_list3
Тип Имя Описание
struct list_head slabspartial Двунаправленный циклический список деск-
дескрипторов участков памяти как со свободны-
свободными, так и с несвободными объектами
struct listhead slabsfull Двунаправленный циклический список деск-
дескрипторов участков памяти без свободных
объектов
struct listhead slabs_free Двунаправленный циклический список деск-
дескрипторов участков памяти только со свобод-
свободными объектами
unsigned long free_objects Количество свободных объектов в кэше
int freetouched Используется алгоритмом утилизации стра-
страничных кадров, который выполняет slab-
аллокатор (см. главу 17)
unsigned long next_reap Используется алгоритмом утилизации стра-
страничных кадров, который выполняет slab-
аллокатор (см. главу 17)
struct array_cache * shared Указатель на локальный кэш, совместно ис-
используемый всеми процессами
Дескриптор участка памяти
Каждый участок памяти в кэше имеет собственный дескриптор типа slab,
поля которого перечислены в табл. 8.10.
Таблица 8.10. Поля дескриптора slab
Тип Имя Описание
struct list_head list Указатели, используемые в одном из трех двуна-
двунаправленных списков дескрипторов участков памяти
(slabs_full, slabs_partial или slabs_free) в
структуре kmem_list3 дескриптора кэша
Таблица 8.10 (окончание)
Тип Имя Описание
unsigned long colourof f Смещение первого объекта в участке памяти
void * smem Адрес первого объекта в участке памяти (либо вы-
выделенного, либо свободного)
unsigned int inuse Количество объектов в участке памяти, используе-
используемых в данный момент (несвободных)
unsigned int free Индекс следующего свободного объекта в участке
памяти или bufctlend, если свободных объектов
не осталось
Дескрипторы участков памяти могут храниться в двух местах:
□ внешний дескриптор участка памяти— хранится вне участка памяти, в
одном из кэшей общего назначения, не подходящем для операций ISA
DMA; на кэш указывает элемент cachesizes;
□ внутренний дескриптор участка памяти — хранится внутри участка памя-
памяти, в начале первого страничного кадра, присвоенного участку.
Slab-аллокатор выбирает второй вариант, когда размер объектов меньше
512 Мбайт, или когда внутренняя фрагментация оставляет в участке памяти
достаточно места для дескриптора участка и для дескрипторов объектов (что
обсуждается далее). Флаг cflgsoffslab в поле flags дескриптора кэша ус-
установлен, если дескриптор участка памяти хранится внутри участка, и сбро-
сброшен в противном случае.
Рис. 8.4 иллюстрирует основные взаимосвязи между дескрипторами кэшей и
дескрипторами участков памяти. Заполненные, частично заполненные и сво-
свободные участки памяти образуют разные списки
Общие и специальные кэши
Кэши бывают двух типов — общие и специальные. Общие кэши используют-
используются только slab-аллокатором для его собственных целей, в то время как специ-
специальные кэши используются остальными компонентами ядра.
Общими кэшами являются следующие:
□ первый кэш, называемый kmemcache, объектами которого являются деск-
дескрипторы остальных кэшей, используемых ядром. Переменная cachecache
содержит дескриптор этого кэша;
Рис. 8.4. Взаимосвязи между дескрипторами кэшей и дескрипторами участков памяти
□ несколько дополнительных кэшей, содержащих области памяти общего
назначения. Диапазон размеров этих областей, как правило, заключает в
себе 13 геометрически распределенных величин. Таблица, называемая
maiiocsizes (элементы которой имеют тип cachesizes), указывает на
26 дескрипторов кэшей, ассоциированных с областями памяти размером
32, 64, 128, 256, 512, 1024, 2 048, 4 096, 8 192, 16 384, 32 768, 65 536 и
131 072 байтов. Для каждого размера имеется два кэша; один подходит
для целей ISA DMA, а другой — для обычного выделения памяти.
Функция krnemcacheinit о вызывается на этапе инициализации системы для
установки общих кэшей.
Специальные кэши создаются функцией kmemcachecreate (). В зависимости
от параметров, она вначале определяет оптимальный способ управления но-
новым кэшем (например, следует ли хранить дескриптор участка памяти внутри
или вне участка). Затем функция выделяет дескриптор для нового кэша из
Общего КЭШа cache__cache И заносит ЭТОТ деСКрИПТОр В СПИСОК cachechain,
являющийся списком дескрипторов кэшей (занесение выполняется после по-
получения семафора cachechainsem, защищающего список от попыток одно-
одновременного обращения).
Существует возможность уничтожить кэш и удалить его из списка
cachechain, ДЛЯ чего вызывается функция kmemcachedestroyO . Эта функ-
ция в основном полезна модулям, создающим собственные кэши при загрузке
и уничтожающим их при выгрузке. Во избежание непроизводительных рас-
расходов памяти, ядро должно уничтожить все объекты-участки памяти до
уничтожения самого кэша. Функция kmemcacheshrink () уничтожает все уча-
участки В КЭШе, ИТераТИВНО ВЫЗЫВаЯ фуНКЦИЮ slab_destroy ().
Имена всех общих и специальных кэшей могут быть прочитаны на этапе вы-
выполнения из файла /proc/slabinfo. Помимо прочего, в этом файле хранится
количество свободных и количество выделенных объектов в каждом кэше.
Интерфейс между slab-аллокатором
и зонным аллокатором страничных кадров
Когда slab-аллокатор создает новый участок памяти, при получении группы
свободных смежных страничных кадров он пользуется услугами зонного ал-
локатора страничных кадров. С этой целью он вызывает функцию
kmemgetpages (), которая в UMA-системе эквивалентна следующему коду:
void * kmem_getpages(kmem_cache_t *cachep, int flags)
{
struct page *page;
int i;
flags |= cachep->gfpflags;
page = alloc_pages(flags, cachep->gfporder);
if (!page)
return NULL;
i = A « cache->gfporder);
if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
atomic_add(i, &slab_reclaim_pages);
while (i—)
SetPageSlab(page++);
return page_address(page);
}
Два параметра этой функции имеют следующий смысл:
□ cachep — указывает на дескриптор кэша, нуждающегося в дополнитель-
дополнительных страничных кадрах (их количество определяется значением порядка в
поле cachep->gfporder);
□ flags — задает способ запрашивания страничного кадра. Этот набор фла-
флагов комбинируется с флагами выделения специального кэша, хранящими-
хранящимися в поле gfpf lags дескриптора кэша.
Размер запрашиваемой памяти задается в поле gfporder дескриптора кэша,
которое кодирует размер участка в кэше7. Если кэш участков памяти был соз-
создан при установленном флаге slabreclaimaccount, страничные кадры, при-
присваиваемые участкам памяти, считаются утилизируемыми страницами, когда
ядро проверяет, достаточно ли памяти для удовлетворения некоторых запро-
запросов в пользовательском режиме. Кроме того, функция устанавливает флаг
PGslab в дескрипторах выделенных страничных кадрах.
В ходе обратной операции страничные кадры, присвоенные участку памяти,
МОГуТ быть ОСВОбоЖДеНЫ С ПОМОЩЬЮ фуНКЦИИ kmemf reepages ():
void kmem_freepages (kmem_cache_t *cachep, void *addr)
{
unsigned long i = (l«cachep->gfporder) ;
struct page *page = virt_to_page(addr);
if (current->reclaim_state)
current->reclaim_state->reclaimed_slab += i;
while (i—)
ClearPageSlab(page++);
free_pages((unsigned long) addr, cachep->gfporder);
if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
atomic_sub (l«cachep->gfporder, &slab_reclaim_pages) ;
}
Эта функция освобождает страничные кадры, начиная с кадра, имеющего ли-
линейный адрес addr и выделенного участку в кэше, идентифицируемом пара-
параметром cachep. Если текущий процесс выполняет утилизацию памяти (поле
current->reclaim_state не содержит NULL), TO ПОЛе reclaimed_slab Структуры
reciaimstate увеличивается должным образом, чтобы только что освобож-
освобожденные страницы были учтены алгоритмом утилизации страничных кадров
(см. разд. "Утилизация при дефиците памяти11 главы 17). Кроме того, если
флаг SLAB_RECLAIM_ACCOUNT установлен, переменная slab_reclaim_pages ДОЛЖ-
ДОЛЖНЫМ образом уменьшается.
7 Обратите внимание на невозможность выделения страничных кадров из зоны памяти
ZONEJHIGHMEM, поскольку функция kmem_getpages () возвращает линейный адрес, полученный от
функции page_address (). Как было разъяснено в разд. "Отображение ядром страничных кадров
верхней памяти"ранее в этой главе, эта функция возвращает NULL для неотображенных странич-
страничных кадров из верхней памяти.
Выделение участка памяти кэшу
Только что созданный кэш не содержит участков памяти и, следовательно, не
содержит свободных объектов. Новые участки присваиваются кэшу, только
когда удовлетворены оба следующих условия:
□ выдан запрос на выделение нового объекта;
□ кэш не содержит ни одного свободного объекта.
В этой ситуации slab-аллокатор присваивает новый участок памяти кэшу, вы-
вызывая функцию cachegrow(). Она, в свою очередь, вызывает функцию
kmemgetpages (), чтобы получить от зонного аллокатора страничных кадров
группу кадров, необходимых для хранения одного участка, а затем — функ-
функцию aiiocsiabrngmt о, чтобы получить новый дескриптор участка. Если флаг
cflgsoffslab дескриптора кэша установлен, дескриптор участка памяти вы-
выделяется из общего кэша, на который указывает поле siabpcache дескрипто-
дескриптора кэша. В противном случае дескриптор участка выделяется в первом стра-
страничном кадре участка памяти.
Ядро должно уметь определять по данному кадру, используется ли он slab-
аллокатором, и, если используется, быстро вычислять адреса соответствую-
соответствующих дескрипторов кэша и участка памяти. Поэтому функция cachegrowo
перебирает дескрипторы всех страничных кадров, присвоенных новому уча-
участку памяти, и записывает в подполя next и prev полей lru этих дескрипторов
адреса дескриптора кэша и дескриптора участка памяти соответственно.
Здесь все корректно, потому что поле lru используется функциями buddy-
системы, только когда страничный кадр свободен, в то время как кадры,
с которыми работают функции slab-аллокатора, имеют установленный флаг
PGslab и не являются свободными с точки зрения buddy-системы8. Ответ на
противоположный вопрос, какие страничные кадры реализуют данный уча-
участок в кэше, может быть получен с помощью поля smem дескриптора участка
памяти и поля gfporder (размер участка) дескриптора кэша.
Затем функция cachegrowo вызывает функцию cacheinitobjs о, которая
применяет метод-конструктор (если он определен) ко всем объектам, содер-
содержащимся в новом участке памяти.
ПОД КОНеЦ работы фунКЦИЯ cachegrow () ВЫЗЫВает фунКЦИЮ listaddtail (),
чтобы добавить только что полученный дескриптор участка *siabp в конец
списка полностью свободных участков, содержащегося в дескрипторе кэша
*cachep, и обновляет счетчик свободных объектов в кэше:
8 Как мы увидим, поле lru используется также алгоритмом утилизации страничных кадров.
list_add_tail(&slabp->list, &cachep->lists->slabs_free);
cachep->lists->free_objects += cachep->num;
Освобождение участка памяти из кэша
Участки памяти могут быть уничтожены в двух случаях:
□ в кэше участков находится слишком много свободных объектов;
□ некоторая периодическая вызываемая функция определяет наличие со-
совершенно неиспользуемых участков, которые могут быть освобождены
(см. главу 17).
В обоих случаях вызывается функция siabdestroyO, которая уничтожает
участок и освобождает соответствующие страничные кадры для зонного ал-
локатора страничных кадров:
void slab_destroy(kmem_cache_t *cachep, slab_t *slabp)
{
if (cachep->dtor) {
int i ;
for (i = 0; i < cachep->num; i++) {
void* objp = slabp->s_mem+cachep->objsize*i;
(cachep->dtor)(objp, cachep, 0);
}
}
kmem_freepages(cachep, slabp->s_mem — slabp->colouroff);
if (cachep->flags & CFLGS_OFF_SLAB)
kmern__cache_free (cachep->slabp_cache, slabp) ;
}
Функция проверяет, имеется ли у кэша метод-деструктор его объектов (поле
dtor должно быть отлично от null), и, если это так, применяет деструктор ко
всем объектам в участке, причем локальная переменная objp отслеживает те-
текущий объект. Затем функция вызывает функцию kmem_f reepages (), которая
возвращает buddy-системе все смежные страничные кадры, использованные
участком. Наконец, если дескриптор участка памяти хранится вне участка,
функция освобождает его из кэша дескрипторов участков.
На самом деле, функция устроена немного сложнее. Например, кэш участков
памяти может быть создан при установленном флаге slabdestroybyrcu,
означающем, что участки должны уничтожаться с отсрочкой, путем регист-
регистрации обратного вызова, с помощью функции caii_rcu() (см. главу 5).
Функция обратного вызова, в свою очередь, вызывает функцию kmem_
f reepages () И, ВОЗМОЖНО, функцию kmemcachef гее (), как В ОСНОВНОМ случае,
описанном ранее.
Дескриптор объекта
У каждого объекта есть короткий дескриптор типа kmembuf ctit. Дескрипто-
Дескрипторы объектов хранятся в массиве, расположенном непосредственно после со-
соответствующего дескриптора участка памяти. Получается, что, подобно де-
дескрипторам участков, дескрипторы объектов участка могут храниться двумя
разными способами, проиллюстрированными на рис. 8.5:
□ внешние дескрипторы объектов — хранятся вне участка памяти, в общем
кэше, на который указывает поле siabpcache дескриптора кэша. Размер
области памяти и, следовательно, конкретного общего кэша, используемо-
используемого для хранения дескрипторов объектов, зависит от количества объектов в
участке памяти (поле num дескриптора кэша);
□ внутренние дескрипторы объектов — хранятся внутри участка, непосред-
непосредственно перед объектами, которые они описывают.
Первый дескриптор объекта в массиве описывает первый объект в участке
памяти и т. д. Дескриптор объекта представляет собой короткое целое без
знака, имеющее смысл, только когда объект свободен. Дескриптор хранит
индекс следующего свободного объекта в участке, реализуя таким способом
простой список свободных объектов внутри участка памяти. Дескриптор объ-
объекта для последнего элемента в списке свободных объектов помечен услов-
условным значением bufctl_end (oxf f ff).
Рис. 8.5. Взаимосвязь между дескрипторами участков и дескрипторами объектов
Выравнивание объектов в памяти
Объекты, с которыми имеет дело slab-аллокатор, выровнены в памяти, т. е.
хранятся в ячейках, начальные физические адреса которых кратны заданной
константе, обычно являющейся степенью двойки. Эта константа называется
фактором выравнивания.
Максимальный фактор выравнивания, разрешенный slab-аллокатором, ра-
равен 4096, т. е. размеру страничного кадра. Это означает, что объекты могут
быть выровнены относительно либо их физических, либо линейных адресов.
В обоих случаях выравнивание может повлиять только на 12 младших битов
адреса.
Обычно в компьютерах скорость обращения к ячейкам памяти выше, когда
физические адреса ячеек выровнены по границе слова (то есть внутренней
ШИНЫ памяти компьютера). В силу ЭТОГО функция kmem_cache_create () ПО
умолчанию выравнивает объекты по границе слова, задаваемой макросом
bytesperword. Для процессоров 80*86 этот макрос возвращает 4, потому
что слово имеет длину 32 бита.
При создании нового кэша участков есть возможность указать, что объекты,
заносимые в него, должны быть выровнены в аппаратном кэше первого уров-
уровня. Для этого ядро устанавливает флаг дескриптора кэша slabhwcachealign.
Функция kmemcachecreate () обрабатывает запрос следующим образом:
□ если размер объекта превышает половину строчки кэша, он выравнивается
в оперативной памяти по границе, кратной licachebytes, т. е. по началу
строчки;
□ в противном случае размер объекта округляется до делителя величины
licachebytes для гарантии того, что маленький объект никогда не пере-
пересечет границу между двумя строчками кэша.
Очевидно, что здесь slab-аллокатор жертвует памятью ради скорости досту-
доступа; он добивается более высокой производительности кэша, искусственно
увеличивая размер объекта и, тем самым, создавая дополнительную внутрен-
внутреннюю фрагментацию.
Окрашивание участков памяти
Из главы 2 мы знаем, что одна и та же строчка аппаратного кэша отображает
много различных блоков оперативной памяти. В этой главе уже говорилось,
что объекты одного размера хранятся в кэше с одним и тем же смещением.
Объекты, имеющие одинаковые смещения внутри разных участков памяти,
с относительно большой вероятностью будут, в конечном счете, отображены
в одну строчку кэша. Следовательно, не исключено, что аппаратный кэш бу-
дет непроизводительно расходовать циклы обращения к памяти, копируя два
объекта из одной строчки кэша в разные места оперативной памяти и обрат-
обратно, в то время как другие строчки кэша будут использованы недостаточно.
Slab-аллокатор старается свести до минимума такое нежелательное поведе-
поведение кэша, проводя политику окрашивания участков памяти: участкам при-
присваиваются разные произвольные значения, называемые цветами.
Прежде чем обсудить окрашивание участков, мы должны рассмотреть схему
расположения объектов в кэше. Возьмем кэш, объекты которого выровнены в
оперативной памяти. Это означает, что адрес объекта должен быть кратен
заданному положительному значению, скажем, ain. Даже с учетом ограниче-
ограничений по выравниванию, существует масса способов разместить объекты внут-
внутри участка памяти. Выбор того или иного способа зависит от решений, при-
принятых в отношении следующих переменных:
□ num— количество объектов, которые могут храниться в участке памяти
(значение этой переменной находится в поле num дескриптора кэша);
□ osize — размер объекта, включая байты выравнивания;
□ dsize — размер дескриптора участка плюс общий размер всех дескрипто-
дескрипторов объектов, с округлением до наименьшего числа, кратного размеру
строчки аппаратного кэша. Значение этой переменной равно 0, если деск-
дескрипторы участка и объектов хранятся вне участка памяти;
□ free — количество неиспользованных байтов (байтов, не присвоенных ни
одному объекту) внутри участка.
Длина участка памяти в байтах может быть выражена следующим образом:
длина участка = (num x osize) + dsize+ free
Значение free всегда меньше osize, потому что в противном случае было бы
возможно разместить в участке дополнительные объекты. Однако free может
быть больше a in.
Slab-аллокатор использует free свободных байтов для окрашивания участка.
Термин "цвет" взят лишь для того, чтобы провести различие между участка-
участками и позволить аллокатору памяти распределить объекты по разным линей-
линейным адресам. В результате ядро "выжимает" максимальную производитель-
производительность из аппаратного кэша процессора.
Участки, имеющие разные цвета, хранят свои первые объекты в разных мес-
местах памяти, удовлетворяя при этом требования по выравниванию. Количество
свободных цветов равно f ree/ain (это значение хранится в поле colour деск-
дескриптора кэша). Таким образом, первый цвет обозначается нулем, а послед-
последний — числом (free/ain)-l. (В частном случае, когда free меньше ain, в поле
colour записывается ноль, для всех участков используется цвет 0, но, в дейст-
действительности, количество цветов равно одному.)
Если участок окрашен цветом col, смещение первого объекта (относительно
начального адреса участка) равно col x ain + dsize байтов. На рис. 8.6 пока-
показано, как расположение объектов внутри участка зависит от цвета этого уча-
участка. Окрашивание фактически приводит к перемещению определенной час-
части свободной области в участке от конца к началу.
Рис. 8.6. Участок с цветом col и выравниванием ain
Окрашивание эффективно, когда значение free достаточно велико. Очевидно,
что если выравнивание объектов не требуется, или количество неиспользуе-
неиспользуемых байтов внутри участка меньше требуемого выравнивания freeoin, то
единственным способом окрашивания участка является окрашивание в нуле-
нулевой цвет, т. е. присваивание нулевого смещения первому объекту.
Различные цвета распределены равномерно среди участков, содержащих объ-
объекты данного типа. Это достигается хранением текущего цвета в поле
coiournext дескриптора кэша. Функция cachegrowo присваивает цвет, оп-
определяемый полем coiournext, новому участку, а затем увеличивает значе-
значение этого поля. Когда достигается значение colour, содержимое поля сбрасы-
сбрасывается в 0. Таким образом, каждый участок создается с цветом, отличным от
предыдущего, и используется максимально возможное количество цветов.
Кроме ТОГО, фуНКЦИЯ cache_grow () Извлекает Значение ain ИЗ ПОЛЯ colour_off
дескриптора кэша, вычисляет dsize по количеству объектов в участке и, на-
наконец, сохраняет значение col x ain + dsize в поле coiouroff дескриптора
участка.
Локальные кэши
свободных объектов-участков памяти
Реализация slab-аллокатора для многопроцессорных систем в Linux 2.6 отли-
отличается от оригинальной реализации Solaris 2.4. Для смягчения борьбы за
спин-блокировку среди процессоров и для повышения эффективности аппа-
аппаратных кэшей, каждый кэш slab-аллокатора содержит структуру (отдельную
для каждого процессора), состоящую из небольшого массива указателей на
освобожденные объекты. Это называется локальным кэшем участка. Боль-
Большинство операций выделения и освобождения объектов участка затрагивает
только локальный кэш, а структуры собственно участка привлекаются только
при переполнении или опустошении локального кэша. Эта техника аналогич-
аналогична описанной ранее в этой главе.
Поле array дескриптора кэша представляет собой массив указателей на
структуры arraycache, по одному элементу для каждого процессора в систе-
системе. Каждая структура arraycache является дескриптором локального кэша
свободных блоков, а ее поля перечислены в табл. 8.11.
Таблица 8.11. Поля структуры array_cache
Тип Имя Описание
unsigned int avail Количество указателей на объекты, доступные в локаль-
локальном кэше. Это поле действует также в качестве индекса
первого свободного слота в кэше
unsigned int limit Размер локального кэша, т. е. максимальное количество
указателей в локальном кэше
unsigned int batchcount Размер пакета объектов при единовременном пополне-
пополнении или опустошении локального кэша
unsigned int touched Флаг, установленный, если локальный кэш был недавно
использован
Обратите внимание, что дескриптор локального кэша не содержит адрес са-
самого кэша. Дело в том, что локальный кэш расположен сразу за своим деск-
дескриптором. Конечно, в локальном кэше хранятся указатели на освобожденные
объекты, а не сами объекты, которые всегда находятся внутри участков памя-
памяти соответствующего кэша.
При создании нового кэша участков функция kmem_cache_create () определяет
размер локальных кэшей (записывая это значение в поле limit дескриптора
кэша), выделяет их и сохраняет указатели на них в поле array дескриптора
кэша.
При создании нового кэша участков функция kmemcachecreate () определяет
размер локальных кэшей (записывая это значение в поле limit дескриптора
кэша), выделяет их и сохраняет указатели на них в поле array дескриптора
кэша. Этот размер зависит от размера объектов, хранящихся в кэше участков,
и колеблется от 1 для очень больших объектов до 120 для маленьких. Кроме
того, начальное значение поля batchcount, содержащего количество объектов,
добавляемых единым пакетом в локальный кэш или удаляемых из него, уста-
устанавливается равным половине размера локального кэша9.
9 Системный администратор может для каждого кэша вручную установить размер локальных кэ-
кэшей и значение поля batchcount, выполнив запись в файл /proc/slabinfo.
В многопроцессорных системах кэши участков для небольших объектов
имеют дополнительный локальный кэш, адрес которого хранится в поле
lists.shared дескриптора кэша. Этот совместно используемый локальный
кэш, как следует из его названия, используется всеми процессорами и облег-
облегчает задачу переноса свободных объектов между локальными кэшами. Его
начальный размер в восемь раз превышает значение в поле batchcount.
Выделение объекта в участке
Новые объекты можно получить, вызывая функцию kmemcacheaiioc (). Па-
Параметр cachep указывает на дескриптор кэша, из которого должен быть взят
новый объект, а параметр flag содержит флаги, передаваемые функциям зон-
зонного аллокатора страничных кадров в случае, если все участки кэша окажутся
заполненными.
Функция эквивалентна следующему коду:
void * kmem_cache_alloc(kmem_cache_t *cachep, int flags)
{
unsigned long save_flags;
void *objp;
struct array_cache *ac;
local_irq_save(save_flags);
ac = cache_p->array[smp_processor_id()];
if (ac->avail) {
ac->touched = 1;
objp = ((void **)(ac+1))[—ac->avail];
} else
objp = cache_alloc_refill(cachep, flags);
local_irq_restore(save_flags);
return objp;
}
Вначале функция пытается получить свободный объект из локального кэша.
Если свободный объект есть в наличии, то поле avail содержит индекс в ло-
локальном кэше, относящийся к элементу, указывающему на последний осво-
освобожденный объект. Поскольку массив локального кэша расположен непо-
непосредственно ПОСЛе деСКрИПТОра ас, фрагмент КОДа ((void**) (ac+1)) [--ас->
avail] извлекает адрес этого свободного объекта и уменьшает значение
ac->avaii. Функция cacheaiiocrefiii о вызывается для пополнения ло-
локального кэша и получения свободного объекта, если свободных объектов
в локальном кэше не оказалось.
Функция cacheaiiocrefiii () выполняет следующие действия:
1. Сохраняет в локальной переменной ас адрес дескриптора локального
кэша:
ас = cachep->array[smp_processor_id()];
2. Получает СПИН-блокирОВКу cachep->spinlock.
3. Если кэш участков содержит совместно используемый локальный кэш, и
этот совместно используемый локальный кэш содержит какое-то количе-
количество свободных объектов, функция пополняет локальный кэш данного
процессора, перенося в него до ac->batchcount указателей из совместно
используемого. Затем она переходит к шагу 6.
4. Пытается пополнить локальный кэш, занося в него до ac->batchcount ука-
указателей на свободные объекты, расположенные в участках кэша:
• просматривает списки siabspartial и siabsfree дескриптора кэша и
получает адрес siabp дескриптора участка, являющегося либо частично
заполненным, либо вовсе пустым. Если такой дескриптор не обнаружи-
обнаруживается, функция переходит к шагу 5;
• для каждого свободного объекта в участке функция увеличивает поле
inuse дескриптора участка, заносит адрес объекта в локальный кэш и
обновляет значение в поле free так, чтобы оно хранило индекс сле-
следующего свободного объекта в участке:
slabp->inuse++;
((void**)(ac+1))[ac->avail++] =
slabp->s_mem + slabp->free * cachep->obj_size;
slabp->free = ((kmem_bufctl_t*)(slabp+1))[slabp->free];
• заносит, если необходимо, очищенный участок в соответствующий
СПИСОК, либо slab_full, Либо slabjoartial.
5. На этом шаге количество указателей, добавленных в локальный кэш, уже
хранится в поле ac->avaii. Функция уменьшает значение поля
freeobjects структуры kmem_list3 именно На ЭТу величину, ОТМечаЯ, ЧТО
эти объекты более не свободны.
6. Освобождает cachep->spinlock.
7. Если сейчас поле ac->avaii больше 0 (имело место пополнение кэша),
функция устанавливает поле ac->touched в единицу и возвращает указа-
указатель на свободный объект, который был последним занесен в локальный
кэш:
return ((void**)(ac+1))[—ac->avail];
8. В противном случае пополнения кэша не было. Функция вызывает функ-
функцию cachegrow () для получения нового участка и, следовательно, новых
свободных объектов.
9. Если работа функции cachegrowo закончилась неудачей, описываемая
функция возвращает null; в противном случае она возвращается к шагу 1
и повторяет всю процедуру.
Освобождение объекта в участке
Функция kmemcachefгее() освобождает объект, ранее выделенный slab-
аллокатором для некоторой функции ядра. Ее параметрами являются cachep,
адрес дескриптора кэша, и objp, адрес освобождаемого объекта:
void kmem_cache_free(kmem_cache_t *cachep, void *objp)
{
unsigned long flags;
struct array_cache *ac;
local__irq_save (flags) ;
ac = cachep->array [sinp_processor_id() ] ;
if (ac->avail == ac->limit)
cache_flusharray(cachep, ac);
((void**)(ac+1))[ac->avail++] = objp;
local_irq_restore(flags);
}
Вначале функция проверяет, имеется ли в локальном кэше место под еще
один указатель на свободный объект. Если это так, указатель добавляется в
локальный кэш, и функция возвращает управление. В противном случае она
сперва вызывает функцию cachef lusharray () для очистки локального кэша,
а затем заносит указатель в локальный кэш.
Функция cachef lusharray () выполняет следующие действия:
1. Получает СПИН-блОКИроВКу cachep->spinlock.
2. Если кэш участков содержит совместно используемый локальный кэш, и
этот совместно используемый локальный кэш еще не заполнен, функция
пополняет совместно используемый локальный кэш, перенося в него до
ac->batchcount указателей из локального кэша данного процессора. Затем
она переходит к шагу 4.
3. Вызывает функцию f reebiock (), чтобы вернуть slab-аллокатору до ас->
batchcount объектов, находящихся в этот момент в локальном кэше. Для
каждого объекта с адресом objp функция выполняет следующие действия:
• увеличивает поле lists. f reeobjects дескриптора кэша;
• определяет адрес дескриптора участка, содержащего объект:
slabp = (struct slab *)(virt_to_page(objp)->lru.prev);
( Примечание )
Вспомним, что поле lru.prev дескриптора страницы участка указывает на со-
соответствующий дескриптор участка.
• удаляет дескриптор участка из списка кэша участков (либо cachep->
lists.slabs_partial, либо cachep->lists.slabs full);
• вычисляет индекс объекта внутри участка:
objnr = (objp — slabp->s_mem) / cachep->objsize;
• сохраняет в дескрипторе объекта текущее значение поля siabp->free и
записывает в поле siabp->f гее индекс объекта (последний освобожден-
освобожденный объект будет выделен первым, когда потребуется):
((kmem__bufctl t *)(slabp+1))[objnr] = slabp->free;
slabp->free = objnr;
• уменьшает поле siabp->inuse;
• если siabp->inuse равно 0 (все объекты в участке свободны), и количе-
количество СВобоДНЫХ объектов ВО Всем КЭШе учаСТКОВ (cachep->
lists, freeobjects) больше максимума, хранящегося в поле саспер->
freeiimit, то функция освобождает страничный кадр (или несколько
кадров) участка памяти для зонного аллокатора страничных кадров:
cachep->lists.free_objects -= cachep->num;
slab_destroy(cachep, slabp);
( Примечание )
Значение в поле cachep->free_limit обычно равно cachep->num +
(l+N) x cachep->batchcount, где N — количество процессоров в системе.
• в противном случае, если значение siab->inuse равно 0, но количество
свободных объектов во всем кэше участков меньше предельного значе-
значения cachep->f reeiiinit, функция вставляет дескрипторы участка в спи-
список cachep->lists.slabs_f гее;
• наконец, если значение siab->inuse больше нуля, значит, участок за-
заполнен ЧаСТИЧНО, И фуНКЦИЯ ЗаНОСИТ еГО ДеСКрИПТОр В СПИСОК cachep->
lists.slabs_partial.
4. Освобождает спин-блокировку cachep->spiniock.
5. Обновляет поле avail дескриптора локального кэша, вычитая количество
объектов, перемещенных в совместно используемый локальный кэш или
освобожденных для slab-аллокатора.
6. Сдвигает все действующие указатели в локальном кэше в начало массива
этого локального кэша. Данный шаг необходим, потому что первые указа-
указатели на объекты были удалены из локального кэша, и остальные должны
быть передвинуты.
Объекты общего назначения
Как было сказано ранее в этой главе, нечастые запросы на области па-
памяти обрабатываются с помощью группы общих кэшей, объекты которых
имеют размеры, геометрически распределенные в диапазоне от 32 до
131 072 байтов.
Объекты этого типа можно получить, вызвав функцию kmaiioco, которая
эквивалентна следующему коду:
void * kmalloc(size_t size, int flags)
{
struct cache_sizes *csizep = malloc_sizes;
kmem_cache_t * cachep;
for (; csizep->cs_size; csizep++) {
if (size > csizep->cs_size)
continue;
if (flags & GFP_DMA)
cachep = csizep->cs_dmacachep;
else
cachep = csizep->cs_cachep;
return kmem_cache_alloc(cachep, flags);
}
return NULL;
}
Функция пользуется таблицей maiiocsizes, чтобы найти размер, являющий-
являющийся степенью двойки и ближайший к запрошенному размеру. Затем она вызы-
вызывает функцию kmemcacheaiioc (), чтобы выделить объект, передавая ей либо
дескриптор кэша для страничных кадров, пригодных для ISA DMA, либо де-
дескриптор кэша для "нормальных" кадров, в зависимости от того, установил
ли вызывающий процесс флаг gfpdma.
Объекты, полученные при помощи функции kmalloc о, могут быть освобож-
освобождены функцией kf гее ():
void kfree(const void *objp)
{
kmem_cache_t * c;
unsigned long flags;
if dobjp)
return;
local_irq_save(flags);
с = (kmem_cache_t *)(virt_to_page(objp)->lru.next);
kmem_cache_free(c, (void *)objp);
local_irq_restore(flags);
}
Нужный дескриптор кэша идентифицируется с помощью подполя lru.next
дескриптора первого страничного кадра, содержащего область памяти. Об-
Область памяти Освобождается функцией kmem_cache_f гее ().
Пулы памяти
Пулы памяти являются новым понятием в Linux 2.6. В принципе, пул памяти
позволяет компоненту ядра (например, подсистеме работы с блочными уст-
устройствами) выделить некоторую динамическую память для использования
только в таких крайних ситуациях, как нехватка памяти.
Не следует путать пулы памяти с зарезервированными страничными кадрами,
описанными ранее в этой главе. Те страничные кадры могут быть использо-
использованы только для удовлетворения атомарных запросов на выделение памяти,
выдаваемых обработчиками прерываний или кодом критических секций. Что
касается пула памяти, он является резервом динамической памяти, который
может быть использован только конкретным компонентом ядра, а именно
"владельцем" пула. В нормальной ситуации владелец не обращается к резер-
резерву. Однако если динамической памяти становится так мало, что все обычные
запросы на выделение памяти обречены на провал, компонент ядра может в
качестве крайнего средства прибегнуть к помощи специальных функций пула
памяти, которые черпают из резерва необходимую память. Таким образом,
создание пула памяти аналогично созданию запаса консервов и использова-
использованию консервного ножа, когда свежие продукты недоступны.
Нередко пул памяти создается в помощь slab-аллокатору, т. е. используется в
качестве резерва объектов для участков памяти. Вообще говоря, пул памяти
можно использовать для выделения любых порций динамической памяти, от
целых страничных кадров до небольших областей, выделяемых с помощью
функции kmaiioc (). Поэтому мы в дальнейшем будем называть единицы па-
памяти, с которыми имеет дело пул памяти, "элементами памяти".
Пул памяти описывается объектом mempooit, поля которого перечислены в
табл. 8.12.
Таблица 8.12. Поля объекта mempool_t
Тип Имя Описание
spinlock_t lock Спин-блокировка, защищающая поля объекта
int min_nr Максимальное количество элементов в пуле
памяти
int curr_nr Текущее количество элементов в пуле памяти
void ** elements Указатель на массив указателей на зарезервиро-
зарезервированные элементы
void * pool_data Закрытые данные, доступные владельцу пула
mempool_alloc_t * alloc Метод для выделения элемента
mempool_free_t * free Метод для освобождения элемента
wait_queue_head_t wait Очередь, используемая, когда пул памяти пуст
В поле minnr хранится начальное количество элементов в пуле памяти.
Иными словами, значение в этом поле представляет количество элементов,
которые владелец пула памяти гарантированно получит от аллокатора памя-
памяти. Поле currnr, которое всегда меньше или равно полю minnr, содержит
количество элементов, находящихся в пуле в данный момент. Сами элементы
памяти доступны через массив указателей, адрес которого хранится в поле
elements.
Методы alloc и free обеспечивают интерфейс с аллокатором памяти, позво-
позволяя, соответственно, получать и освобождать элементы памяти. Оба метода
могут быть собственными функциями компонента ядра, который владеет пу-
пулом памяти.
Когда элементы памяти являются объектами участка, методы alloc и free
обычно реализуются функциями mempool_alloc_slab () И mempool_f ree_slab (),
которые просто вызывают функции kmem_cache_alloc() И kmemcachef гее ()
соответственно. В этом случае поле pooidata объекта mempooit содержит
адрес дескриптора кэша участков.
Функция mempooicreate () создает новый пул памяти. Она принимает в каче-
качестве параметров количество элементов minnr, адреса функций, реализующих
методы alloc и free, и, возможно, значение поля pooidata. Функция выделя-
выделяет память под объект mempooit и массив указателей на элементы памяти, и
затем многократно вызывает метод alloc для получения minnr элементов
памяти. Функция mempooidestroy (), наоборот, освобождает все элементы в
пуле, а затем — массив элементов и объект mempooit.
Чтобы выделить элемент из пула памяти, ядро вызывает функцию
mernpooiaiioco, передавая ей адрес объекта mempooit и флаги выделения
памяти (см. табл. 8.5 и 8.6). В сущности, функция пытается выделить элемент
памяти с помощью аллокатора памяти, вызывая метод aiioc в соответствии с
флагами, которые она получила в качестве параметров. Если выделение па-
памяти проходит успешно, функция возвращает элемент памяти, не затрагивая
пул памяти. В противном случае она берет элемент из пула. Конечно, боль-
большое количество операций выделения в условиях дефицита памяти может ис-
исчерпать пул. В таком случае, если флаг gfpwait установлен, функция
mempooiaiioco блокирует текущий процесс, пока какой-нибудь элемент па-
памяти не будет освобожден для пула памяти.
И наоборот, чтобы освободить элемент для пула памяти, ядро вызывает
ФУНКЦИЮ mempoolf гее (). ЕСЛИ пул памяти не заполнен (значение currjnin
меньше, чем minnr), функция добавляет этот элемент в пул. В противном
случае mempoolf гее () вызывает метод free, чтобы освободить элемент для
аллокатора памяти.
Управление
несмежными областями памяти
Мы уже знаем, что предпочтительно отображать области памяти в последо-
последовательности смежных страничных кадров, тем самым обеспечивая более эф-
эффективное использование кэша и уменьшая среднее время доступа к памяти.
Впрочем, если запросы на области памяти выдаются нечасто, есть смысл рас-
рассмотреть схему выделения, основанную на несмежных страничных кадрах,
обращение к которым происходит через смежные линейные адреса. Основное
достоинство этой схемы в том, что она позволяет избежать внешней фраг-
фрагментации, а недостатком является необходимость обращаться к Таблицам
Страниц ядра. Ясно, что размер несмежной области памяти должен быть кра-
кратен 4096. Linux по-разному использует несмежные области памяти — напри-
например, для выделения структур для активных областей подкачки (см.
разд. "Перевод области подкачки в активное и неактивное состояние" гла-
главы 17), для выделения места под модуль (см. приложение 2) или для выделе-
выделения буферов каким-нибудь драйверам ввода/вывода. Кроме прочего, несмеж-
несмежные области памяти предоставляют еще один способ использования странич-
страничных кадров из верхней памяти (см. разд. "Выделение несмежной области
памяти" далее в этой главе).
Линейные адреса несмежных областей памяти
Чтобы найти свободный интервал линейных адресов, мы можем просмотреть
область, начинающуюся с адреса pageoffset (обычно это Охсооооооо, начало
четвертого гигабайта). На рис. 8.7 показано, как используются линейные ад-
адреса четвертого гигабайта:
П начало этой области включает в себя линейные адреса, отображающие
первые 896 Мбайт оперативной памяти, а линейный адрес, который соот-
соответствует концу физической памяти, отображенной напрямую, хранится
В переменной highjnemory;
□ конец области содержит фиксированно отображенные линейные адреса;
□ начиная с адреса pkmapbase, мы видим линейные адреса, используемые
для постоянного отображения ядром страничных кадров верхней памяти;
□ остальные линейные адреса можно использовать под смежные области
памяти. Интервал размером 8 Мбайт (макрос vmalloc_offset) вставляется
в целях безопасности между концом отображения физической памяти и
первой областью памяти. Его предназначение— "ловить" обращения к
памяти, нарушающие установленные границы. По той же причине допол-
дополнительные интервалы по 4 Кбайт вставляются для разделения несмежных
областей памяти.
Рис. 8.7. Интервал линейных адресов, начинающийся с PAGE_OFFSET
Макрос vmallocstart определяет начальный адрес линейного пространства,
зарезервированного под несмежные области памяти, а макрос vmallocjend
определяет его конечный адрес.
Дескрипторы несмежных областей памяти
Каждая несмежная область памяти ассоциирована с дескриптором типа
vmstruct, поля которого перечислены в табл. 8.13.
Эти дескрипторы организованы в простой список при помощи поля next.
Адрес первого элемента списка хранится в переменной vmiist. Обращения к
Таблица 8.13. Поля дескриптора vm_struct
Тип Имя Описание
void * addr Линейный адрес первой ячейки области
unsigned long size Размер области плюс 4096 (интервал безопасно-
безопасности между областями)
unsigned long flags Тип памяти, отображаемой несмежной областью
памяти
struct page ** pages Указатель на массив из nr_pages указателей на
дескрипторы страниц
unsigned int nr_pages Количество страниц, занимаемых областью
unsigned long phys_addr Поле содержит 0, если область не была создана
для отображения совместно используемой памя-
памяти аппаратного устройства
struct vm_struct * next Указатель на следующую структуру vm_struct
ЭТОМу СПИСКУ регулируются СПИН-блЛОКИрОВКОЙ ЧТеНИя/заПИСИ vmlist_lock.
Поле flags идентифицирует тип памяти, отображаемый областью: VMALLOC
для страниц, получаемых с помощью функции vmaiioco, vmmap для уже
выделенных страниц, отображенных функцией vmapo, и vmioremap для
встроенной памяти аппаратных устройств, отображенных при помощи функ-
функции ioremap () (см. главу 13).
Функция getvmarea () ищет свободный интервал линейных адресов между
vmalloc_start и vmalloc_end. Эта функция принимает два параметра:
□ size — размер создаваемой области в байтах;
□ flag — флаг, задающий тип памяти.
Функция выполняет следующие действия:
1. Вызывает функцию kmaiioc (), чтобы получить область памяти для нового
дескриптора типа vmstruct.
2. Получает блокировку vmlistlock и просматривает список дескрипторов
типа vmstruct в поисках свободного интервала линейных адресов, вклю-
включающего, как минимум, size + 4096 адресов D096 — это размер интерва-
интервала безопасности между областями памяти).
3. Если такой интервал существует, функция инициализирует поля дескрип-
дескриптора, освобождает блокировку vmlistlock и завершает работу, возвращая
начальный адрес несмежной области памяти.
4. В противном случае функция getvmarea () освобождает ранее получен-
полученный дескриптор, освобождает блокировку vmiistiock и возвращает null.
Выделение несмежной области памяти
Функция vmaiioco выделяет ядру несмежную область памяти. Параметр size
задает размер запрошенной области. Если функция в состоянии удовлетво-
удовлетворить запрос, она возвращает начальный линейный адрес новой области.
В противном случае функция возвращает null:
void * vmallос(unsigned long size)
{
struct vm_struct *area;
struct page **pages;
unsigned int array_size, i;
size = (size + PAGE_SIZE - 1) & PAGE_MASK;
area = get_vm_area(size, VM_ALLOC);
if (larea)
return NULL;
area->nr_pages = size » PAGE_SHIFT;
array_size = (area->nr_pages * sizeof(struct page *));
area->pages = pages = kmalloc(array_size, GFP KERNEL);
if (!area_pages) {
remove_vm_area (area->addr) ;
kfree(area);
return NULL;
}
memset(area->pages, 0, array_size);
for (i=0; i<area->nr_pages; i++) {
area->pages[i] = alloc_page(GFP_KERNEL| GFPJHIGHMEM);
if (!area->pages[i]) {
area->nr_pages = i;
fail: vfree(area->addr);
return NULL;
}
}
if (map_vm_area(area, pgprot@x63), &pages))
goto fail;
return area->addr;
}
Функция начинает с округления значения параметра size до числа, кратно-
кратного 4096 (размер страничного кадра). Затем функция vmaiioco вызывает
функцию getvmarea (), которая создает новый дескриптор и возвращает ли-
нейный адрес, присвоенный области памяти. Поле flags дескриптора ини-
инициализируется флагом vmalloc, и это означает, что несмежные области па-
памяти будут отображены в интервал линейных адресов с помощью функции
vmalloc (). Затем функция vmalloc () вызывает функцию kmalloc (), Чтобы СДе-
лать запрос на группу смежных страничных кадров, достаточно большую для
хранения массива указателей на дескрипторы страниц. Функция memset () вы-
вызывается для сброса всех этих указателей в значение null. После этого мно-
многократно вызывается функция alloc_page(), ПО разу на каждую ИЗ nrjoages
страниц области, для выделения страничного кадра и сохранения адреса со-
соответствующего дескриптора страницы в массиве area->pages. Обратите
внимание, что применение массива area->pages необходимо, потому что
страничные кадры могут принадлежать зоне памяти zonehighmem, т. е. в дан-
данный момент они необязательно отображены в линейный адрес.
А теперь — самое интересное. К этому моменту получен "свежий" интервал
смежных линейных адресов, и выделена группа несмежных страничных кад-
кадров для отображения этих адресов. Последний, и самый важный, шаг вклю-
включает в себя манипуляции с записями таблиц страниц, используемых ядром,
для обозначения того факта, что каждый страничный кадр, выделенный не-
несмежной области памяти, теперь ассоциирован с линейным адресом, входя-
входящим в интервал смежных линейных адресов, который был получен от функ-
функции vmalloc (). Именно ЭТИМ И Занимается фуНКЦИЯ map_vm_area ().
Функция mapvmarea () принимает три параметра:
□ area — указатель на дескриптор vmstruct данной области;
□ prot— биты защиты выделенных страничных кадров. Параметр всегда
равен ОхбЗ, ЧТО соответствует битам Present, Accessed, Read/Write И Dirty;
□ pages — адрес переменной, указывающей на массив указателей на деск-
дескрипторы страниц (то есть как тип данных используется struct page ***).
Функция начинает с того, что присваивает линейные адреса начала и конца
области памяти переменным address и end соответственно:
address = area->addr;
end = address + (area->size — PAGE_SIZE);
Вспомним, что поле area->size хранит фактический размер области плюс
4 Кбайт (размер интервала безопасности между областями). Функция приме-
применяет макрос pgdof fsetk, чтобы извлечь запись из главного глобального ка-
каталога страниц ядра, относящуюся к начальному линейному адресу области,
а затем получает спин-блокировку Таблицы Страниц:
pgd = pgd_offset_k(address);
spin_lock(&init_mm.page_table_lock);
Затем функция входит в следующий цикл:
int ret = 0;
for (i = pgd_index(address); i < pgd_index(end-1); i++) {
pud_t *pud = pud_alloc(&init_mm, pgd, address);
ret = -ENOMEM;
if (!pud)
break;
next = (address + PGDIR_SIZE) & PGDIR_MASK;
if (next < address || next > end)
next = end;
if (map_area_pud(pud, address, next, prot, pages))
break;
address = next;
pgd++;
ret = 0;
}
spin_unlock(&init_mm.page_table_lock);
flush_cache_vmap((unsigned long)area->addr, end);
return ret;
На каждом шаге цикла функция вначале вызывает функцию pudaiioc (), что-
чтобы создать верхний каталог страниц для новой области, и заносит его физи-
физический адрес в соответствующую запись глобального каталога страниц ядра.
Затем она вызывает функцию mapareapud (), чтобы выделить все таблицы
страниц, ассоциированные с новым верхним каталогом страниц. Она склады-
складывает размер диапазона линейных адресов, охватываемого одним верхним ка-
каталогом страниц (это константа 230, если механизм РАЕ включен, и 222 в про-
противном случае), с текущим значением переменной address и продвигает ука-
указатель pgd на глобальный каталог страниц.
Цикл повторяется, пока не будут заполнены все записи Таблицы Страниц,
ссылающиеся на несмежную область памяти.
Функция mapareapud () выполняет аналогичный цикл для всех таблиц стра-
страниц, на которые указывает верхний каталог страниц:
do {
pmd_t * pmd = pmd_alloc(&init_mm, pud, address);
if (!pmd)
return -ENOMEM;
if (map_area_pmd(pmd, address, end-address, prot, pages))
return -ENOMEM;
address = (address + PUD_SIZE) & PUD_MASK;
pud++;
} while (address < end);
Функция mapareapmd () выполняет аналогичный цикл для всех таблиц стра-
страниц, на которые указывает средний каталог страниц:
do {
pte_t * pte = pte_alloc_kernel (&init_mm, pmd, address);
if (!pte)
return -ENOMEM;
if (map_area_pte(pte, address, end-address, prot, pages))
return -ENOMEM;
address = (address + PMD_SIZE) & PMD_MASK;
pmd++;
} while (address < end);
Функция pteaiiockernei () выделяет новую Таблицу Страниц и обновляет
соответствующую запись в среднем каталоге страниц. Затем функция
mapareapte () выделяет все страничные кадры, соответствующие записям
в Таблице Страниц. Значение переменной address увеличивается на 222 (раз-
(размер интервала линейных адресов, покрываемого одной Таблицей Страниц), и
цикл повторяется.
Главный ЦИКЛ функции mapareapte () ВЫГЛЯДИТ так:
do {
struct page * page = **pages;
set_pte(pte, mk_pte(page, prot));
address += PAGE_SIZE;
pte++;
(*pages)++;
} while (address < end);
Адрес page дескриптора страничного кадра, подлежащего отображению, чи-
читается из элемента массива, на который указывает переменная с адресом
pages. Физический адрес нового страничного кадра записывается в Таблицу
Страниц макросами setpte и mkpte. После прибавления константы4096
(длина страничного кадра) к переменной address цикл повторяется.
Обратите внимание, что Таблицы Страниц текущего процесса не затрагива-
затрагиваются функцией mapvmarea (). Поэтому, когда процесс в режиме ядра обра-
обращается к несмежной области памяти, возникает исключение "ошибка обра-
обращения к странице" из-за того, что записи в Таблицах Страниц процесса, соот-
соответствующие данной области, содержат нули. Однако обработчик этого
исключения сверяет линейный адрес, вызвавший ошибку, с главными
Таблицами Страниц ядра (которыми являются глобальный каталог страниц
initjnm.pgd и его дочерние таблицы страниц; см. главу 2). После того как об-
обработчик исключения обнаружит, что главная Таблица Страниц ядра содер-
жит ненулевую запись для данного адреса, он копирует ее в соответствую-
соответствующую запись Таблицы Страниц процесса и возобновляет нормальное выпол-
выполнение процесса. Этот механизм описан в главе 9.
Помимо функции vmaiiocO несмежную область памяти может выделить
функция vmaiioc_32 (). Она очень похожа на vmailoc (), но выделяет странич-
страничные кадры из зон памяти zone_normal и zone_dma.
В Linux 2.6 есть еще и функция vmap(), которая отображает страничные кад-
кадры, уже выделенные в несмежной области памяти. Эта функция принимает в
качестве параметра массив указателей на дескрипторы страниц, вызывает
ФУНКЦИЮ getvmarea (), чтобы ПОЛучИТЬ НОВЫЙ дескриптор vmstruct, а Затем
вызывает функцию mapvmarea (), чтобы отобразить страничные кадры. Та-
Таким образом, функция vmapo аналогична функции vmaiiocO, но не выделяет
страничные кадры.
Освобождение
несмежной области памяти
Функция vf гее () освобождает несмежные области памяти, созданные функ-
функциями vmailoc о или vmaiioc_32 (), а функция vunmapo освобождает области
памяти, созданные функцией vmapo. Обе функции принимают один пара-
параметр — адрес первого линейного адреса области, подлежащей освобожде-
освобождению, причем реальную работу для обеих функций выполняет функция
vunmap ().
Функция vunmap () принимает два параметра: addr — адрес первого линей-
линейного адреса области, подлежащей освобождению, и флаг deaiiocate_pages,
который установлен, если страничные кадры, отображенные в область, долж-
должны быть освобождены для зонного аллокатора страничных кадров (вызов
фуНКЦИИ vfree()), И сброшен В ПрОТИВНОМ Случае (ВЫЗОВ фуНКЦИИ vunmap ()).
Функция выполняет следующие действия:
1. Вызывает функцию remove_vm_area (), чтобы ПОЛучИТЬ адрес area дескрип-
тора vmstruct и очистить записи таблиц страниц ядра, соответствующие
линейному адресу в несмежной области памяти.
2. Если флаг deaiiocatepages установлен, функция просматривает массив
area->pages указателей на дескрипторы страниц. Для каждого элемента
массива она вызывает функцию f reepage (), чтобы освободить странич-
страничный кадр для зонного аллокатора страничных кадров. Кроме того, вызыва-
вызывается функция kfree (area->pages) с целью освобождения самого массива.
3. Вызывает фуНКЦИЮ kfree (area), чтобы ОСВОбоДИТЬ Дескриптор vmstruct.
ФуНКЦИЯ reruovevmarea () ВЫПОЛНЯет Следующий ЦИКЛ:
write_lock(&vmlist_lock);
for (р = &vmlist ; (trnp = *р) ; р = &tmp->next) {
if (tmp->addr == addr) {
unmap_vm_area (tmp) ;
*p = tmp->next;
break;
}
}
write_unlock(&vmlist_lock) ;
return tmp;
Сама область освобождается при помощи функции unmapvmarea(). Эта
функция принимает единственный параметр, указатель area на дескриптор
области vmstruct. Она выполняет следующий цикл, чтобы совершить дейст-
действия, противоположные деЙСТВИЯМ фуНКЦИИ map_vm_area ():
address = area->addr;
end = address + area->size;
pgd = pgd_offset_k(address);
for (i = pgd_index(address); i <= pgd_index(end-1); i++) {
next = (address + PGDIR_SIZE) & PGDIR_MASK;
if (next <= address I I next > end)
next = end;
unmap_area_pud(pgd, address, next — address);
address = next;
pgd++;
}
В свою очередь, функция unmapareapud () выполняет действия, обратные
действиям функции mapareapud (), в следующем цикле:
do {
unmap_area_pmd(pud, address, end-address);
address = (address + PUDJSIZE) & PUD_MASK;
pud++;
} while (address && (address < end));
ФуНКЦИЯ unmapareapmd () обращает деЙСТВИЯ фуНКЦИИ mapareapmd () В ЦИКЛе:
do {
unmap_area_pte(pmd, address, end-address);
address = (address + PMD_SIZE) & PMD_MASK;
pmd++;
} while (address < end);
Наконец, функция unmap_area_pte() обращает действия функции map_area_
pte () в цикле:
do {
pte_t page = ptep get_and_clear(pte);
address += PAGE_SIZE;
pte++;
if (!pte_none(page) && !pte_present(page))
printk("Whee. . . Swapped out page in kernel page tableW);
} while (address < end);
На каждом шаге цикла макрос ptepgetandciear обнуляет запись таблицы
страниц, на которую указывает переменная pte.
Так же как и в функции vmaiioco, ядро модифицирует записи главного гло-
глобального каталога страниц ядра и его дочерних таблиц страниц, но оно ос-
оставляет без изменений записи в таблицах страниц процесса, которые отобра-
отображают четвертый гигабайт. Это правильно, поскольку ядро никогда не утили-
утилизирует верхние и средние каталоги страниц, а также Таблицы Страниц, корни
которых находятся в главном глобальном каталоге страниц ядра.
В качестве примера предположим, что процесс в режиме ядра обратился к
несмежной области памяти, которая впоследствии была освобождена. Записи
глобального каталога страниц, принадлежащего процессу, равны соответ-
соответствующим записям глобального каталога страниц ядра в результате работы
механизма, описанного в главе 9; они указывают на одни и те же верх-
верхние и средние каталоги страниц, а также Таблицы Страниц. Функция
unmapareapte () очищает только записи таблиц страниц (без утилизации са-
самих таблиц). Последующие обращения процесса к освобожденной несмеж-
несмежной области памяти вызовут исключение "ошибка обращения к странице",
поскольку записи в таблицах страниц будут содержать одни нули. Однако
обработчик исключения сочтет такое обращение действительно ошибочным,
потому что главные таблицы страниц ядра не содержат правильных записей.
ГЛАВА 9
Адресное пространство
процесса
Как видно из предыдущей главы, функции ядра получают динамическую па-
память весьма незамысловатым способом, вызывая одну из множества специ-
специальных функций: getf reepages () ИЛИ allocjpages () ДЛЯ получения СТра-
НИЦ ОТ ЗОННОГО алЛОКатора СТраниЧНЫХ кадров, kmem_cache_alloc() ИЛИ
kmaiiocO, чтобы воспользоваться slab-аллокатором для специализированных
Объектов ИЛИ объектов Общего Назначения, И vmalloc() ИЛИ vmalloc_32() ДЛЯ
выделения несмежных областей памяти. Если запрос может быть удовлетво-
удовлетворен, любая из этих функций возвращает адрес дескриптора страницы либо
линейный адрес, идентифицирующий начало выделенной динамической об-
области памяти.
Такой простой подход работает по двум причинам:
□ ядро является самым приоритетным компонентом операционной системы.
Если функция ядра запрашивает динамическую память, значит, у нее есть
на то веские основания, и не следует отклонять этот запрос;
□ ядро доверяет самому себе. Предполагается, что в функциях ядра нет про-
программных ошибок, и ядру не нужно защищаться от них.
Совершенно иная ситуация при выделении памяти процессам, работающим в
режиме пользователя:
□ запросы процессов на динамическую память не считаются неотложными.
Например, когда загружается исполняемый файл процесса, маловероятно,
что процесс обратится ко всем страницам кода в ближайшем будущем.
Аналогичным образом, когда процесс вызывает библиотечную функцию
maiiocO, чтобы получить дополнительную динамическую память, это не
означает, что он вскоре обратится к полученной памяти. Поэтому в каче-
стве общего правила ядро старается отсрочить выделение динамической
памяти процессам режима пользователя;
□ поскольку программам пользователей доверять нельзя, ядро должно быть
готово к обработке всех ошибок адресации, вызванных работой процессов
в режиме пользователя.
Как будет видно из этой главы, ядру удается отложить выделение динамиче-
динамической памяти процессам за счет применения ресурсов нового типа. Когда про-
процесс в режиме пользователя запрашивает динамическую память, он не полу-
получает дополнительные страничные кадры. Вместо этого он получает право ис-
использовать новый диапазон линейных адресов, который становится частью
его адресного пространства. Этот диапазон называется областью памяти.
В следующем разделе мы обсудим, как процесс "видит" динамическую па-
память. Затем мы опишем основные компоненты адресного пространства про-
процесса. После этого мы подробно рассмотрим роль, которую играет исключе-
исключение "ошибка обращения к странице" в откладывании выделения страничных
кадров процессам, и проиллюстрируем, как ядро создает и удаляет целые ад-
адресные пространства процессов. В конце главы мы обсудим API-интерфейсы
и системные вызовы, связанные с управлением адресным пространством.
Адресное пространство процесса
Адресное пространство процесса состоит из всех линейных адресов, к кото-
которым процессу разрешено обращаться. Каждый процесс видит свое множество
линейных адресов; адрес, используемый одним процессом, не имеет никакого
отношения к адресу, используемому другим. Как мы увидим позже, ядро мо-
может динамически изменять адресное пространство процесса, добавляя или
удаляя интервалы линейных адресов.
Ядро представляет интервалы линейных адресов с помощью ресурсов, назы-
называемых областями памяти и характеризуемых начальным линейным адре-
адресом, длиной и правами доступа. Из соображений эффективности начальный
адрес и длина области памяти должны быть кратны 4096, чтобы данные,
идентифицируемые любой областью памяти, целиком заполняли страничные
кадры, выделенные для нее. Перечислим некоторые типичные ситуации, в
которых процесс получает новые области памяти:
□ когда пользователь вводит команду с консоли, процесс оболочки создает
новый процесс для выполнения команды. Новому процессу выделяется
новое адресное пространство и, следовательно, некоторое множество об-
областей памяти (см. главу 20);
П работающий процесс может в какой-то момент загрузить совершенно дру-
другую программу. В этом случае идентификатор процесса остается прежним,
но области памяти, использованные до загрузки программы, освобожда-
освобождаются, и процессу выделяется новое множество областей памяти (см.
разд. "Функции exec"главы 20);
П работающий процесс может выполнить так называемое отображение фай-
файла (или его части) в память. В таких случаях ядро выделяет процессу но-
новую область памяти для отображения файла (см. разд. "Отображение в
память" главы 16);
П процесс может добавлять данные в свой стек режима пользователя, пока
не будут исчерпаны все адреса в области памяти, отображающие стек. То-
Тогда ядро может принять решение увеличить размер области памяти (см.
разд. "Обработчик исключения "ошибка обращения к странице''" далее в
этой главе);
□ процесс может создать совместно используемую область памяти как меха-
механизм межпроцессного взаимодействия, чтобы использовать данные совме-
совместно с другими процессами. В этом случае ядро выделит процессу новую
область памяти для реализации этой конструкции (см. разд. "Совместно
используемая память IP С" главы 19);
П процесс может попытаться расширить свою динамическую область (кучу)
с помощью, например, функции maiioco. Вследствие этого ядро может
принять решение увеличить размер области памяти, выделенной под кучу
(см. разд. "Управление кучей" далее в этой главе).
В табл. 9.1 приведены некоторые системные вызовы, имеющие отношение к
ситуациям, перечисленным в списке. Вызов brk () обсуждается в конце главы,
а остальные подробно описываются в других главах.
Таблица 9.1. Системные вызовы, связанные с созданием
и удалением областей памяти
Системный вызов Описание
brk () Изменяет размер кучи процесса
execve () Загружает новый исполняемый файл, меняя тем самым адрес-
адресное пространство процесса
_exit () Завершает выполнение текущего процесса и уничтожает его
адресное пространство
fork () Создает новый процесс и, следовательно, новое адресное
пространство
mmap (), mmap2 () Создает отображение файла в память, увеличивая тем самым
адресное пространство процесса
mremap () Расширяет или сужает область памяти
remap_f ile_pages () Создает нелинейное отображение файла (см. главу 16)
Таблица 9.1 (окончание)
Системный вызов Описание
munmapO Уничтожает отображение файла в память, уменьшая тем са-
самым адресное пространство процесса
shmat () Присоединяет совместно используемую область памяти
shmdt () Отсоединяет совместно используемую область памяти
Как мы увидим позже, ядру важно идентифицировать области памяти, ис-
используемые в данный момент процессом (то есть адресное пространство про-
процесса), потому что это позволит обработчику исключения "ошибка обраще-
обращения к странице" эффективно различать два типа ошибочных адресов, приво-
приводящих к этому исключению:
□ адреса, указанные вследствие программной ошибки;
□ адреса, принадлежащие отсутствующей странице; хотя линейный адрес
принадлежит адресному пространству процесса, страничный кадр, соот-
соответствующий этому адресу, еще должен быть выделен.
Адреса второго типа не являются ошибочными с точки зрения процесса. Воз-
Возникающее исключение "ошибка обращения к странице" эксплуатируется
ядром для реализации схемы "выделение страниц по требованию": ядро пре-
предоставляет процессу недостающий страничный кадр и позволяет ему про-
продолжить выполнение.
Дескриптор памяти
Вся информация относительно адресного пространства процесса заключена в
объекте, который имеет тип mmstruct и называется дескриптором памяти.
На этот объект указывает поле mm дескриптора процесса. Поля дескриптора
памяти перечислены в табл. 9.2.
Таблица 9.2. Поля дескриптора памяти
Тип Поле Описание
struct vm_area_struct * ramap Указатель на голову списка объектов-
областей памяти
struct rb_root ram_rb Указатель на корень красно-черного
дерева
struct vm_area_struct * mmap_cache Указатель на объект-области памяти,
к которому производилось последнее
обращение
Таблица 9.2 (продолжение)
Тип Поле Описание
unsigned long (*)() get_unmapped_area Метод для поиска доступного интер-
интервала линейных адресов в адресном
пространстве процесса
void (*) () unmaparea Метод, вызываемый при освобожде-
освобождении интервала линейных адресов
unsigned long mmap_base Идентифицирует линейный адрес
первой выделенной анонимной об-
области памяти или отображения файла
в память (см. главу 20)
unsigned long f ree_area_cache Адрес, начиная с которого ядро будет
искать свободный интервал линейных
адресов в адресном пространстве
процесса
pgd_t * pgd Указатель на глобальный каталог
страниц
atomic_t mm_users Вторичный счетчик обращений
atomic_t mm_count Главный счетчик обращений
int mapcount Количество областей памяти
struct rw_semaphore mmap_sem Семафор чтения/записи для областей
памяти
spinlock_t page_table_lock Спин-блокировка для областей
памяти и Таблиц Страниц
struct list_head iranlist Указатели на соседние элементы
в списке дескрипторов памяти
unsigned long start_code Начальный адрес исполняемого кода
unsigned long endcode Конечный адрес исполняемого кода
unsigned long start_data Начальный адрес инициализирован-
инициализированных данных
unsigned long end_data Конечный адрес инициализированных
данных
unsigned long start_brk Начальный адрес кучи
unsigned long brk Текущий конечный адрес кучи
unsigned long start_stack Начальный адрес стека режима
пользователя
unsigned long arg_start Начальный адрес аргументов команд-
командной строки
unsigned long argend Конечный адрес аргументов команд-
командной строки
Таблица 9.2 (продолжение)
Тип Поле Описание
unsigned long env_start Начальный адрес переменных
окружения
unsigned long env_end Конечный адрес переменных
окружения
unsigned long rss Количество страничных кадров,
выделенных процессу
unsigned long anon_rss Количество страничных кадров,
выделенных анонимным отображени-
отображениям в память
unsigned long total_vm Размер адресного пространства про-
процесса (количество страниц)
unsigned long locked_vm Количество "заблокированных" стра-
страниц, которые не могут быть выгруже-
выгружены (см. главу 17)
unsigned long shared_vm Количество страниц в совместно
используемых отображениях файлов
в память
unsigned long exec_vm Количество страниц в исполняемых
отображениях в память
unsigned long stack_vm Количество страниц в стеке режима
пользователя
unsigned long reserved_vm Количество страниц в зарезервиро-
зарезервированных или специальных областях
памяти
unsigned long def_f lags Флаги прав доступа к областям памя-
памяти, принятые по умолчанию
unsigned long nr_ptes Количество Таблиц Страниц у данного
процесса
unsigned long [ ] saved_auxv Используется при запуске
ELF-программы (см. главу 20)
unsigned int dumpable Флаг, определяющий, может ли про-
процесс выполнять дамп памяти
cpumask_t cpu_vm_mask Битовая маска для переключателей
"ленивого" режима TLB (см. главу 2)
ram_context_t context Указатель на таблицу с информацией,
специфичной для архитектуры
(например, адрес LDT в архитектуре
80x86)
Таблица 9.2 (окончание)
Тип Поле Описание
unsigned long swap_token_time Момент времени, в который процесс
станет кандидатом на обладание
жетоном защиты от выгрузки
(см. главу17)
char recent_pagein Флаг, который устанавливается, если
недавно возникла ошибка обращения
к странице
int core_waiters Количество облегченных процессов,
выполняющих дамп адресного про-
пространства процесса в файл core
(см. разд. "Удаление адресного про-
пространства процесса" далее в этой
главе)
struct completion * core_startup_done Указатель на структуру completion
(см. главу 5)
struct completion core_done Структура completion, используемая
при создании core-файла
rwlock_t ioctx_list_lock Блокировка, применяемая для защиты
списка контекстов асинхронного вво-
ввода/вывода (см. главу 16)
struct kioctx * ioctx_list Список контекстов асинхронного вво-
ввода/вывода (см. главу 16)
struct kioctx def ault_kioctx Контекст асинхронного ввода/вывода,
принятый по умолчанию (см. главу 16)
unsigned long hiwater_rss Максимальное количество странич-
страничных кадров, когда-либо принадле-
принадлежавших процессу
unsigned long hiwater_vm Максимальное количество страниц,
когда-либо находившихся в областях
памяти процесса
Все дескрипторы памяти хранятся в двунаправленном списке. Каждый деск-
дескриптор содержится в поле mmiist дескриптора памяти initmm, используемого
процессом 0 на этапе инициализации. В многопроцессорных системах список
защищен от попыток одновременного обращения спин-блокировкой
mmlist_lock.
Поле mmusers содержит количество облегченных процессов, совместно ис-
использующих структуру mmstruct (см. главу 3). Поле mmcount является глав-
главным счетчиком обращений дескриптора памяти, причем все "пользователи" в
поле mmusers считаются за одного в поле mmcount. Каждый раз, когда поле
mmcount уменьшается, ядро проверяет, достигнуто ли нулевое значение. Если
поле стало равно нулю, дескриптор уничтожается, потому что необходимость
в нем отпала.
Попробуем На Примере объЯСНИТЬ раЗНИЦу Между ПОЛЯМИ mm_users И mm_count.
Пусть некий дескриптор памяти совместно используется двумя облегченны-
облегченными процессами. В обычной ситуации поле mmusers содержит значение 2, а
поле immcount — значение 1 (два процесса-владельца считаются за один).
Если дескриптор памяти временно одалживается потоку ядра (см. следующий
раздел), ядро увеличивает поле mmcount. Таким образом, даже если оба об-
облегченных процесса закончат выполнение, и поле mmusers станет равным О,
дескриптор памяти не будет освобожден, пока им пользуется поток ядра, по-
потому что поле mmcount остается больше 0.
Если ядру нужна уверенность в том, что дескриптор памяти не будет освобо-
освобожден в середине длительной операции, оно может увеличить значение в поле
mmusers, анев ПОЛе mmcount (именно ЭТО делает функция trytounuse (); СМ.
разд. "Перевод области подкачки в активное и неактивное состояние" гла-
главы 17). Конечный результат будет таким же, потому что увеличение поля
mmusers гарантирует, что поле mmcount не сравняется с нулем, даже если все
облегченные процессы, владеющие данным дескриптором памяти, закончат
работу.
Функция mmaiioco вызывается, когда нужно получить новый дескриптор
памяти. Поскольку эти дескрипторы хранятся в кэше slab-аллокатора, функ-
функция mmallocO ВЫЗЫВает функцию kmem_cache_alloc(), инициализирует НО-
НОВЫЙ Дескриптор ПаМЯТИ И Записывает 1 В ПОЛЯ mmcount И mmusers.
Функция mmputo, наоборот, уменьшает значение поля mmusers дескриптора
памяти. Когда оно достигает 0, функция освобождает локальную таблицу де-
дескрипторов, дескрипторы областей памяти и Таблицы Страниц, на которые
ссылается дескриптор памяти. Затем она вызывает функцию mmdrop (). По-
Последняя уменьшает счетчик mmcount и, если он становится равным нулю, ос-
освобождает структуру mmstruct.
ПОЛЯ mmap, mm_rb, mmlist И mmap_cache обсуждаЮТСЯ В следующем разделе.
Дескриптор памяти для потоков ядра
Потоки ядра работают только в режиме ядра, поэтому они никогда не обра-
обращаются к линейным адресам ниже tasksize (или, что то же самое,
pageoffset; обычно это значение равно Охсооооооо). В отличие от обычных
процессов, потоки ядра не пользуются областями памяти, и, следовательно,
большинство полей дескриптора памяти бессмысленно для них.
Поскольку записи в таблице страниц, ссылающиеся на линейные адреса вы-
выше tasksize, всегда должны быть идентичны, то, по большому счету, нет
разницы, какими Таблицами Страниц пользуется поток ядра. Чтобы избежать
ненужных сбросов на диск кэшей и TLB-буферов, поток ядра использует на-
набор Таблиц Страниц последнего активного обычного процесса. С этой целью
в каждый дескриптор процесса включены два указателя на дескрипторы па-
памяти: mm И active_mm.
Поле mm дескриптора процесса указывает на дескриптор памяти, принадле-
принадлежащий данному процессу, а поле activemm— на дескриптор, используемый
процессом, когда он выполняется. В случае обычных процессов эти два поля
содержат один и тот же указатель. Однако потоки ядра не владеют дескрип-
дескрипторами памяти, и у них поле mm всегда содержит null. Когда поток ядра вы-
выбирается для выполнения, его поле activemm инициализируется значением из
поля activemm процесса, выполнявшегося перед этим (см. разд. "Фунщия
schedule ()" главы 7).
Однако здесь есть одна тонкость. Как только процесс в режиме ядра модифи-
модифицирует запись в Таблице Страниц, относящуюся к "верхнему" линейному ад-
адресу (выше tasksize), он должен обновить соответствующую запись в набо-
наборах Таблиц Страниц всех процессов в системе. На самом деле, отображение,
однажды заданное процессом в режиме ядра, должно быть действительно и
для всех остальных процессов в режиме ядра. Модификация наборов Таблиц
Страниц всех процессов является дорогостоящей операцией, и в Linux принят
подход, при котором она отсрочивается.
Мы уже упоминали такой подход в главе 8. Каждый раз, когда возникает
необходимость в новом отображении верхнего линейного процесса (как
правило, это происходит при вызове функции vmaiioco или vfreeo), ядро
обновляет канонический набор Таблиц Страниц, уходящий корнями в
swapperpgdir, т. е. главном глобальном каталоге страниц ядра (см. главу 2).
На этот глобальный каталог страниц указывает поле pgd главного дескрипто-
дескриптора памяти, хранящегося в переменной initmm1.
Далее, в разд. "Обработка обращений к несмежным областям памяти", мы
опишем, как обработчик исключения "ошибка обращения к странице" забо-
заботится о распространении информации, хранящейся в каноническом наборе
Таблиц Страниц, когда в этом возникает реальная необходимость.
1 Ранее мы говорили, что процесс swapper обращается к дескриптору памяти initmm на этапе
инициализации. Однако он больше не пользуется этим дескриптором после завершения инициали-
инициализации.
Области памяти
Область памяти реализуется в Linux с помощью объекта, имеющего тип
vmareastruct. Его поля перечислены в табл. 9.32.
Таблица 9.3. Поля объекта "область памяти"
Тип Поле Описание
struct mm_struct * vm_ram Указатель на дескриптор памя-
памяти, который владеет областью
unsigned long vm_start Первый линейный адрес внутри
области
unsigned long vm_end Первый линейный адрес за
пределами области
struct vm_area_struct * vm_next Следующая область в списке
процесса
pgprot_t vm_page_prot Права доступа, установленные
для страничных кадров области
unsigned long vm_f lags Флаги области
struct rbjnode vm_rb Данные для красно-черного
дерева (см. далее в этой главе)
union shared Ссылки на структуры, исполь-
используемые при обратном отобра-
отображении (см. главу 17)
struct list_head anon_vma_node Указатели на список анонимных
областей памяти (см. главу 17)
struct anon_vma * anon_vma Указатель на структуру
anon_vma (см. главу 17)
struct vm_operations_struct* vm_ops Указатель на методы области
памяти
unsigned long vm_pgof f Смещение в отображенном
файле (см. главу 16). Для ано-
анонимных страниц это либо О,
либо значение
vm_st art / PAGE_S IZE
struct file * vm_file Указатель на файловый объект
отображенного файла, если
таковой имеется
void * vm_private_data Указатель на закрытые данные
области памяти
2 Мы опустили ряд полей, используемых в системах NUMA.
Таблица 9.3 (окончание)
Тип Поле Описание
unsigned long vm_truncate_count Используется при освобожде-
освобождении интервала линейных адре-
адресов в нелинейном отображении
файла в память
Каждый дескриптор области памяти идентифицирует интервал линейных ад-
адресов. Поле vmstart содержит первый линейный адрес этого интервала, а
поле vmend — первый линейный адрес за пределами интервала. Таким обра-
образом, выражение vmend-vmstart определяет длину области памяти. Поле
vminm указывает на дескриптор памяти mmstruct процесса, владеющего дан-
данной областью памяти. Остальные поля объекта vmareastruct мы будем опи-
описывать по мере необходимости.
Области памяти, принадлежащие процессу, никогда не перекрываются, а яд-
ядро старается объединять их, когда новая область выделяется непосредственно
рядом с уже существующими. Две смежных области памяти могут быть объ-
объединены, если права доступа к ним совпадают.
Как видно из рис. 9.1, когда новый интервал линейных адресов добавляется в
адресное пространство процесса, ядро проверяет, можно ли увеличить уже
имеющуюся область памяти (рис. 9.1, а). Если нельзя, создается новая об-
область памяти (рис. 9.1,6). Аналогичным образом, если интервал линейных
адресов удаляется из адресного пространства процесса, ядро изменяет разме-
размеры областей памяти, затронутых этой операцией (рис. 9.1, в). В некоторых
случаях изменение размера приводит к разбиению области памяти на две
(рис. 9.1, гK.
Поле vmops указывает на структуру vmoperationsstruct, которая содержит
методы, определенные для области памяти. Только четыре метода (они пере-
перечислены в табл. 9.4) применимы в UMA-системах.
Таблица 9.4. Методы для работы с областью памяти
Метод Описание
open Вызывается, когда область памяти добавляется к набору областей, при-
принадлежащих процессу
close Вызывается, когда область памяти удаляется из набора областей, принад-
принадлежащих процессу
3 Теоретически удаление интервала линейных адресов может закончиться неудачей, если для ново-
нового дескриптора памяти не найдется свободной памяти.
Таблица 9А (окончание)
Метод Описание
nopage Вызывается обработчиком исключения "ошибка обращения к странице",
когда процесс пытается обратиться к странице, отсутствующей в опера-
оперативной памяти, но линейный адрес этой страницы принадлежит области
памяти
populate Вызывается для заполнения записей таблицы страниц, соответствующих
линейным адресам области памяти (предварение возможных ошибок об-
обращения). Используется, в основном, для нелинейного отображения фай-
файлов в память
Рис. 9.1. Добавление или удаление интервала линейных адресов
Структуры данных для областей памяти
Все области, принадлежащие одному процессу, объединены в простой спи-
список. Они упорядочены в нем по возрастанию адресов, однако между двумя
областями, соседними в списке, в памяти может существовать область с не-
используемыми адресами. Поле vmnext каждого элемента vmareastruct
указывает на следующий элемент списка. Ядро находит области памяти с по-
помощью поля mmap дескриптора памяти, принадлежащего процессу. Это поле
содержит указатель на первый дескриптор области памяти в списке.
Поле mapcount дескриптора памяти содержит количество областей, принад-
принадлежащих процессу. По умолчанию процесс может иметь до 65 536 различных
областей памяти, но системный администратор может изменить это значение,
записав новое в файл /proc/sys/vm/max_map_count.
На рис. 9.2 показана взаимосвязь между адресным пространством процесса,
его дескриптором памяти и списком областей памяти.
Рис. 9.2. Дескрипторы, связанные с адресным пространством процесса
Одной из операций, часто выполняемых ядром, является поиск области памя-
памяти, включающей в себя конкретный линейный адрес. Поскольку список от-
отсортирован, поиск можно завершить, как только будет найдена область памя-
памяти, заканчивающаяся после заданного линейного адреса.
Однако использование списка удобно только в том случае, когда у процесса
очень мало областей памяти, скажем, менее нескольких десятков. Поиск,
вставка и удаление элементов включают в себя ряд более мелких операций,
время выполнения которых пропорционально длине списка.
Хотя большинство процессов в Linux имеет мало областей памяти, бывают
крупные приложения, например, объектно-ориентированные базы данных
или специализированные отладчики для контроля обращений к функции
maiioco, которым принадлежат сотни и даже тысячи областей. В таких слу-
случаях сопровождение списка областей памяти становится очень неэффектив-
неэффективным, и производительность системных вызовов, связанных с обращением к
памяти, падает до недопустимо низкого уровня.
По этим причинам в Linux 2.6 дескрипторы памяти хранятся в структурах,
называемых красно-черными деревьями. В красно-черном дереве у каждого
элемента (узла) обычно имеется два потомка: левый и правый. Узлы дерева
отсортированы. Для каждого узла N все элементы поддерева, имеющего ко-
корень в его левом потомке, предшествуют этому узлу, а все узлы поддерева с
корнем в правом потомке узлаЖ, следуют за этим узлом (см. рис. 9.3, а).
Ключ узла хранится в самом узле. Кроме того, красно-черное дерево должно
удовлетворять четырем дополнительным условиям:
1. Каждый узел должен быть либо красным, либо черным.
2. Корень дерева должен быть черным.
3. Потомки красного узла должны быть черными.
4. Любой путь от некоторого узла до низлежащего листа дерева должен
включать в себя одно и то же количество черных узлов.
При подсчете черных узлов нулевые указатели считаются черными уз-
узлами.
Рис. 9.3. Примеры красно-черных деревьев
Эти четыре пункта гарантируют, что любое красно-черное дерево с п внут-
внутренними узлами имеет высоту не более 2 х \og(n + 1).
Поиск элемента в красно-черном дереве очень эффективен, потому что вклю-
включает в себя операции, время выполнения которых пропорционально логариф-
логарифму от размера дерева. Иными словами, удвоение количества областей памяти
добавит лишь одну итерацию в операцию.
Вставка и удаление элемента красно-черного дерева тоже очень эффективны,
потому что алгоритм может быстро пройти по дереву и найти позицию, в ко-
торую следует вставить элемент или из которой следует удалить элемент.
Каждый новый узел вставляется как лист и окрашивается красным. Если эта
операция нарушит требования, предъявляемые к дереву, некоторые узлы
придется передвинуть или окрасить в другой цвет.
Предположим, например, что элемент со значением 4 должен быть вставлен в
красно-черное дерево, изображенное на рис. 9.3, а. Его позицией будет пра-
правый потомок узла с ключом 3, но после вставки получится, что красный узел
со значением 3 имеет красного потомка, что противоречит третьему требова-
требованию. Чтобы удовлетворить его, меняется цвет узлов 3, 4 и 7. Но эта операция
нарушает требование 4, и алгоритм выполняет "ротацию" в поддереве, кор-
корнем которого является узел с ключом 19. В результате получается новое
красно-черное дерево, изображенное на рис. 9.3, б. Все это кажется сложным,
но вставка и удаление элемента красно-черного дерева требует небольшого
количества операций, пропорционального логарифму от размера дерева.
Итак, для хранения областей памяти, принадлежащих процессу, в Linux при-
применяется как связный список, так и красно-черное дерево. Обе структуры со-
содержат указатели на одни и те же дескрипторы областей памяти. При вставке
или удалении дескриптора области памяти ядро ищет в красно-черном дереве
предыдущий и следующий элементы и с их помощью быстро обновляет спи-
список, не сканируя его.
Ссылка на голову списка содержится в поле mmap дескриптора памяти. Каж-
Каждый объект-область памяти хранит в поле vmnext указатель на следующий
элемент списка. Ссылка на корень красно-черного дерева содержится в поле
mmrb дескриптора памяти. Каждый объект-область памяти хранит в поле
vmrb (имеющем тип rbnode) цвет узла и указатели на родителя, на левого
потомка и на правого потомка.
Вообще говоря, красно-черное дерево применяется для поиска области,
включающей определенный адрес, а список, в основном, полезен при скани-
сканировании всего множества областей.
Права доступа к области памяти
Прежде чем продолжать, мы хотим прояснить связь между страницей и обла-
областью памяти. Как было сказано в главе 2, мы используем термин "страница"
для обозначения как множества линейных адресов, так и данных, хранящихся
по этим адресам. В частности, мы говорим об интервале линейных адресов
от 0 до 4095, что это страница 0, интервал от 4096 до 8191 является страни-
страницей 1, и т. д. Таким образом, каждая область памяти состоит из набора стра-
страниц, имеющих последовательные номера.
Мы уже обсуждали два вида флагов, ассоциированных со страницей:
□ флаги, например, Read/Write, Present ИЛИ User/Supervisor, хранящиеся
в каждой записи Таблицы Страниц (см. главу 2);
П флаги, хранящиеся в поле flags каждого дескриптора страницы (см. главу 8).
Первый вид флагов используется в архитектуре 80x86 для проверки допус-
допустимости запрошенного типа адресации; второй используется в Linux для са-
самых разных целей (см. табл. 8.2).
Сейчас мы представим третий вид флагов — флаги, ассоциированные со
страницами области памяти. Они хранятся в поле vmfiags дескриптора
vmareastruct (табл. 9.5). Некоторые флаги отражают информацию, которой
обладает ядро, относительно всех страниц области памяти, например, что эти
страницы содержат, а также какие права доступа к ним есть у процесса. Дру-
Другие флаги описывают собственно область, например, каким образом она мо-
может расширяться.
Таблица 9.5. Флаги области памяти
Имя флага Описание
vmread Страницы доступны для чтения
vmwrite Страницы доступны для записи
vmexec Страницы доступны для выполнения
vmshared Страницы доступны для совместного использования несколькими
процессами
vm_mayread Флаг vm_read может быть установлен
vm_maywrite флаг vm_write может быть установлен
vm_mayexec флаг vm_exec может быть установлен
vm_mayshare флаг vm_ share может быть установлен
vmgrowsdown Область может расширяться в направлении меньших адресов
vm_growsup Область может расширяться в направлении больших адресов
vm_shm Область применяется в качестве совместно используемой памяти
в рамках IPC
vmdenywrite Область отображает файл, который не может быть открыт для записи
vm_executable Область отображает исполняемый файл
vm_locked Страницы области заблокированы и не могут быть выгружены
vm_io Область отображает адресное пространство ввода/вывода для неко-
некоторого устройства
vmseqread Приложение обращается к страницам последовательно
Таблица 9.5 (окончание)
Имя флага Описание
vm_rand_read Приложение обращается к страницам в случайном порядке
vm_dontcopy He копировать область при ответвлении нового процесса
vm_dontexpand Запретить расширение области с помощью системного вызова
mremap()
vm_reserved Область является специальной (например, это адресное пространст-
пространство ввода/вывода для некоторого устройства), поэтому страницы нель-
нельзя выгружать
vm_account Проверять, достаточно ли свободной памяти для отображения при
создании области совместно используемой памяти IPC (см. главу 19)
vm_hugetlb Страницы в области обрабатываются механизмом выделения
расширенных страниц
vm_nonlinear Область реализует нелинейное отображение
Права доступа к странице, хранящиеся в дескрипторе области памяти, можно
комбинировать произвольным образом. Например, сделать так, чтобы стра-
страницы области были доступны для чтения, но не выполнения. Чтобы эффек-
эффективно реализовать подобную схему защиты, следует продублировать права
на чтение, запись и выполнение, ассоциированные с областью памяти, во
всех соответствующих записях Таблицы Страниц. Тогда проверки будут вы-
выполняться непосредственно блоком управления страницами процессора.
Иными словами, права доступа к странице диктуют, какие попытки обраще-
обращения к странице приведут к возникновению соответствующего исключения.
Как мы вскоре убедимся, работа по выяснению, что именно вызвало ошибку
обращения к странице, делегируется операционной системой Linux обработ-
обработчику этого исключения, в котором реализовано несколько различных страте-
стратегий.
Начальные значения флагов Таблицы Страниц (которые должны быть одина-
одинаковыми у всех страниц в области памяти, о чем мы уже говорили) хранятся в
поле vmpageprot дескриптора vmareastruct. При добавлении страницы
ядро устанавливает флаги в соответствующих записях Таблицы Страниц со-
согласно содержимому ПОЛЯ vmpageprot.
Однако трансляция прав доступа к области памяти в биты защиты страницы
не является простой операцией по следующим причинам:
□ в некоторых случаях попытка обращения к странице должна вызывать со-
соответствующее исключение, даже если данный тип доступа прописан в
поле vm_f lags дескриптора области памяти. Например, как мы увидим да-
далее в этой главе, ядро может принять решение о хранении двух идентич-
ных закрытых (то есть со сброшенными флагами vmshare) страниц, дос-
доступных для записи и принадлежащих двум разным процессам, в одном
страничном кадре. В этом случае исключение должно быть сгенерировано,
когда один из процессов попытается модифицировать страницу;
□ как было сказано ранее, у процессоров 80x86 Таблицы Страниц имеют
всего два бита защиты, а именно флаги Read/Write И User/Supervisor.
Кроме того, флаг user/supervisor каждой страницы, входящей в область
памяти, должен быть всегда установлен, потому что страница должна
быть доступна процессам режима пользователя;
П современные микропроцессоры Pentium 4 при включенном механизме вы-
выделения страниц РАЕ пользуются флагом nx (No eXecute, Выполнение за-
запрещено) в каждой 64-битовой записи Таблицы Страниц.
Если ядро было откомпилировано без поддержки РАЕ, в Linux приняты сле-
следующие правила (они преодолевают аппаратные ограничения микропроцес-
микропроцессоров 80x86):
□ право на чтение всегда подразумевает право на выполнение, и наоборот;
□ право на запись всегда подразумевает право на чтение.
Если же ядро было скомпилировано с поддержкой РАЕ, и у центрального
процессора есть флаг nx, в Linux принимаются другие правила:
□ право на выполнение всегда подразумевает право на чтение;
□ право на запись всегда подразумевает право на чтение.
Кроме того, чтобы корректно применить технику "копирование при чтении"
для отсрочки выделения страничных кадров, страничный кадр защищается от
записи, когда соответствующая страница не должна совместно использовать-
использоваться несколькими процессами.
Таким образом, 16 возможных комбинаций прав на чтение, запись, выполне-
выполнение и совместное использование сводятся к минимуму, в соответствии со
следующими правилами:
□ если для страницы определены права на запись и совместное использова-
использование, устанавливается бит Read/Write;
□ если для страницы определено право на чтение или выполнение, но не оп-
определено право на запись или совместное использование, бит Read/write
сбрасывается;
□ если бит nx поддерживается процессором, а для страницы не определено
право на выполнение, бит nx устанавливается;
□ если для страницы не определено никаких прав доступа, бит Present сбра-
сбрасывается, чтобы любая попытка обращения к ней вызывала соответст-
вующее исключение. Однако чтобы отличать эту ситуацию от той,
когда страница действительно отсутствует, в Linux устанавливается бит
4
Page size .
Биты защиты, соответствующие каждой комбинации прав доступа, хранятся
В 16 элементах массива protection_map.
Работа с областями памяти
Получив базовое представление о структурах данных и информации о со-
состоянии, которые управляют работой с памятью, мы можем обсудить группу
функций нижнего уровня, которые манипулируют дескрипторами областей
памяти. Их следует считать вспомогательными функциями, упрощающими
реализацию функций dommap () и domunmap (). Эти две функции, описанные в
разд. "Выделение интервала линейных адресов" и "Освобождение интервала
линейных адресов" далее в этой главе, увеличивают и, соответственно,
уменьшают адресное пространство процесса. Работая на уровне более высо-
высоком, чем функции, рассматриваемые в этом разделе, они принимают в каче-
качестве параметров не дескриптор области памяти, а начальный адрес и длину
интервала линейных адресов и права доступа к нему.
Поиск области, ближайшей к данному адресу:
функция find_vma()
Функция f indvma () принимает два параметра: адрес mm дескриптора памяти
процесса и линейный адрес addr. Она находит первую область памяти, у ко-
которой значение в поле vmend больше, чем addr, и возвращает адрес его деск-
дескриптора. Если такой области нет, функция возвращает null. Обратите внима-
внимание, что область, найденная функцией f indvma (), необязательно включает в
себя addr, потому что этот адрес может находиться и за ее пределами.
Каждый дескриптор памяти имеет поле mmapcache, где хранится адрес облас-
области, к которой процесс обращался в прошлый раз. Это дополнительное поле
введено для сокращения времени поиска области, содержащей данный ли-
линейный адрес. Расположение ссылок на адреса в программах таково, что если
последний проверенный адрес принадлежал данной области, то вероятность
попадания следующего адреса в ту же область очень велика.
4 Читатель, возможно, назовет подобное использование бита Page size нечестным трюком, по-
поскольку данный бит предназначен для индикации реального размера страницы. Однако Linux не
пострадает от такого обмана, потому что процессор 80><86 проверяет бит Page size в записях ката-
каталога страниц, а не в записях Таблицы Страниц.
Итак, функция начинается с проверки, содержит ли область, идентифицируе-
идентифицируемая полем mmapcache, адрес addr. Если это так, она возвращает указатель на
дескриптор области:
vma = ram->ramap_cache;
if (vma && vma->vm_end > addr && vma->vm_start <= addr)
return vma;
В противном случае необходим перебор областей памяти, принадлежащих
процессу, и функция ищет область памяти в красно-черном дереве:
rb_node = mm->mm_rb.rb_node;
vma = NULL;
wh i 1 e (rb__node) {
vma_tmp = rb_entry (rb_node, struct vm_area_struct, vm__rb) ;
if (vma_tmp->vm_end > addr) {
vma = vma__tmp;
if (vma_tmp->vm_start <= addr)
break;
rb node = rb_node->rb left;
} else
rbjnode = rb_node->rb_right;
}
if (vma)
mm->mmap cache = vma;
return vma;
Функция вызывает макрос rbentry, который по указателю на узел красно-
черного дерева вычисляет адрес соответствующего дескриптора области па-
памяти.
Функция f ind_vma jorev () аналогична фуНКЦИИ f indvma () С ТОЙ разницей, ЧТО
она записывает в дополнительный параметр pprev указатель на дескриптор
области памяти, предшествующей той, которую нашла функция.
Наконец, функция findvmaprepareo находит позицию нового листа в крас-
красно-черном дереве, который соответствует данному линейному адресу, и воз-
возвращает адрес предшествующей области памяти и адрес родителя вставляе-
вставляемого узла.
Поиск области, пересекающейся с данным интервалом:
функция find_vmajntersection()
Функция findvmaintersectiono ищет первую область памяти, которая пе-
ресекается с данным интервалом линейных адресов. Параметр mm указывает
на дескриптор памяти процесса, а линейные адреса startaddr и endaddr за-
задают интервал:
vma = f ind_vma (ram, start_addr) ;
if (vma && end_addr <= vma->vm_start)
vma = NULL;
return vma;
Если область не будет найдена, функция возвратит указатель null. Точнее
говоря, если функция f indvma () возвратила корректный адрес, но найденная
область памяти начинается после окончания интервала линейных адресов,
переменная vma получает значение null.
Поиск свободного интервала:
функция get_unmapped_area()
Функция getunmappedarea () просматривает адресное пространство процесса
с целью найти свободный интервал линейных адресов. Параметр len задает
длину интервала, а ненулевой параметр addr, когда он не равен 0, указывает
адрес, с которого должен начаться поиск. Если поиск увенчается успехом,
функция возвратит начальный адрес нового интервала; в противном слу-
случае КОД ОШИбкИ -ENOMEM.
Проверив, что параметр addr не равен null, функция убеждается, что указан-
указанный адрес находится в адресном пространстве режима пользователя и что
он выровнен по границе страницы. Затем она вызывает один из двух методов,
в зависимости от цели использования интервала адресов: для отображения
файла в память или для анонимного отображения. В первом случае функция
выполняет файловую операцию getunmappedarea (см. главу 16).
Во втором случае функция выполняет метод getunmappedarea дескриптора
Памяти. ЭТОТ МеТОД реализован Либо функцией arch_get_unmapped_area (), ЛИ-
бо archgetunmappedareatopdown (), В СООТВетСТВИИ СО Схемой области Па-
мяти. Как мы увидим в разд. "Сегменты программы и области памяти про-
процесса" главы 20, у любого процесса может быть две разных схемы располо-
расположения областей памяти, выделенных системным вызовом mmap (): либо они
начинаются с линейного адреса Ох4ооооооои растут вверх, либо они начина-
начинаются выше стека режима пользователя и растут в направлении меньших
адресов.
Рассмотрим функцию archgetunmappedarea (), Которая ИСПОЛЬЗуетСЯ, КОГДа
области памяти выделяются в направлении от нижних адресов к верхним.
Она эквивалентна следующему фрагменту кода:
if (len > TASKJ3IZE)
return -ENOMEM;
addr = (addr + Oxfff) & OxfffffOOO;
if (addr && addr + len <= TASK_SIZE) {
vma = find vma (current->rnm, addr);
if (!vma || addr + len <= vma->vm start)
return addr;
}
start_addr = addr = mm->free_area_cache;
for (vma = find_vma(current->mm, addr); ; vma = vma->vm_next) {
if (addr + len > TASK_SIZE) {
if (start_addr = (TASK_SIZE/3+0xfff)&0xfffff000)
return -ENOMEM;
start_addr = addr = (TASK_SIZE/3+0xfff)&0xfffffOOO;
vma = find_vma (current->mm, addr);
}
if (!vma || addr + len <= vma->vm_start) {
mm->free_area_cache = addr + len;
return addr;
}
addr = vma->vm_end;
}
Функция начинается с проверки, не превышает ли длина интервала число
tasksize, являющееся максимальным значением для линейных адресов ре-
режима пользователя (как правило, 3 Гбайт). Если значение addr отлично от
нуля, функция пытается выделить интервал, начиная с адреса addr. На всякий
случай, функция округляет значение addr до числа, кратного 4 Кбайт.
Если параметр addr равен 0, или предыдущий поиск закончился неудачей,
функция archgetunmappedarea() просматривает пространство линейных
адресов режима пользователя в поисках диапазона линейных адресов, не
входящего ни в одну область памяти и достаточно большого, чтобы содер-
содержать новую область. Для ускорения поиска его начальная точка обычно уста-
устанавливается на линейный адрес, следующий за последней выделенной об-
областью памяти. Поле mm->free_area_cache дескриптора памяти ИНИЦИалиЗИ-
руется значением, равным одной трети от пространства линейных адресов
режима пользователя (как правило, 1 Гбайт), а затем обновляется по мере
создания новых областей памяти. Если функции не удается найти подходя-
подходящий интервал линейных адресов, поиск возобновляется с начала, т. е. с одной
трети пространства линейных адресов режима пользователя. Дело в том, что
первая треть этого пространства зарезервирована под области памяти с пре-
предопределенными линейными адресами; обычно туда входят текст, данные и
bss-сегменты исполняемого файла (см. главу 20).
Функция вызывает функцию f indvma () для обнаружения первой области па-
памяти, которая заканчивается после начальной точки поиска. Затем она по-
вторно просматривает все последующие области памяти. Здесь возможны три
случая:
□ запрошенный интервал больше, чем еще не просмотренная часть про-
пространства линейных адресов (addr + len > tasksize). Тогда функция ли-
либо возобновляет поиск с одной трети адресного пространства режима
пользователя, либо, если повторный поиск уже производился, возвращает
-enomem (недостаточно линейных адресов для удовлетворения запроса);
□ свободного места за последней просмотренной областью не хватает
(vma != NULL && vma->vm_start < addr + len). Функция анализирует СЛе-
дующую область;
□ ни одно из предыдущих условий не выполнено, и было найдено достаточ-
достаточно большое свободное место. Функция возвращает addr.
Занесение области в список дескрипторов памяти:
функция insert_vm__struct()
Функция insertvmstruct () ЗаНОСИТ Структуру vm_area_struct В СПИСОК объ-
ектов-областей памяти и в красно-черное дерево дескриптора памяти. Она
принимает два параметра: mm, который задает адрес дескриптора памяти про-
процесса, и vma, который задает адрес вставляемого объекта vmareastruct. Поля
vmstart и vmend объекта-области памяти должны уже быть проинициализи-
рованы. Функция вызывает функцию f indvmaprepare () для поиска позиции
в красно-черном дереве mm->mm_rb, в которую должен быть вставлен объект
vma. Затем функция insertvmstruct () ВЫЗЫВает функцию vma_link(), KOTO-
рая выполняет следующие действия:
1. Заносит область памяти в связный список, на который указывает поле
mm— >mmap.
2. Заносит область памяти в красно-черное дерево mm->mm_rb.
3. Если область памяти является анонимной, функция заносит область в спи-
список, голова которого находится в соответствующей структуре anonvma
(см. разд. "Обратное отображение для анонимных страниц" главы 17).
4. Увеличивает счетчик mm->map_count.
Если область содержит файл, отображенный в память, функция vmaiinko
выполняет дополнительные операции, которые описаны в главе 17.
Функция _vma_uniink() принимает в качестве параметров адрес дескриптора
памяти mm и два адреса объектов-областей памяти — vma и prev. Обе области
памяти должны принадлежать дескриптору mm, и область prev должна
предшествовать области vma. Функция удаляет объект vma из списка и
красно-черного дерева дескриптора памяти. Она также обновляет поле
mm->mmap_cache, содержащее адрес области памяти, к которой процесс обращал-
обращался последний раз, если это поле указывает на только что удаленную область.
Выделение интервала линейных адресов
Теперь обсудим, как выделяются новые интервалы линейных адресов. Функ-
Функция dommap () создает и инициализирует новую область памяти для процесса
current. После успешного выделения область памяти может быть слита с
другими областями, принадлежащими процессу.
Функция работает со следующими параметрами:
□ указатель file на файловый объект и смещение offset внутри файла ис-
используются, когда новая область памяти должна отображать файл. Эта те-
тема обсуждается в главе 16. В этом разделе мы предполагаем, что отобра-
отображение файла в память не происходит, и оба параметра равны null;
□ addr — этот линейный адрес определяет, откуда должен начаться поиск
свободного интервала;
□ len — длина интервала линейных адресов;
□ prot — этот параметр задает права доступа к страницам, включенным в
область памяти. Возможными флагами являются protread, protwrite,
protexec и protnone. Первые три означают то же самое, что флаги
vm_read, vm_write и vm_exec. Флаг prot_none показывает, что у процесса нет
ни одного из этих прав;
□ flag — этот параметр задает остальные флаги области памяти:
• MAP_GROWSDOWN, MAPJLOCKED, MAP_DENYWRITE И МАР_ЕХЕ CUT ABLE СМЫСЛ
этих флагов аналогичен смыслу флагов, перечисленных в табл. 9.5;
• mapshared и mapprivate — первый флаг означает, что страницы в об-
области памяти могут быть совместно использованы несколькими про-
процессами, а второй имеет противоположный эффект. Оба флага связаны
С флаГОМ VM_SHARED В Дескрипторе vm_area_struct;
• mapfixed — начальный линейный адрес интервала должен в точности
совпадать с адресом, переданным в параметре addr;
• map_anonymous — с областью памяти не ассоциирован никакой файл (см.
главу 16);
• mapnoreserve— функция не обязана производить предварительную
проверку свободных страничных кадров;
• mappopulate — функция должна заранее выделить страничные кадры,
необходимые для отображения, устанавливаемого областью памяти.
Этот флаг имеет значение только для областей, отображающих файлы
(см. главу 16), и областей совместно используемой памяти IPC (см. гла-
главу 19);
• mapnonblock— имеет значение, только когда установлен флаг мар_
populate: во время предварительного выделения страничных кадров
функцию нельзя блокировать.
Функция doramapo выполняет некоторые предварительные проверки значе-
значения параметра offset, а затем вызывает функцию dommappgoff(). В этой
главе мы предполагаем, что выделяемый интервал линейных адресов не бу-
будет отображать файл на диске — отображение файлов подробно обсуждается
в главе 16. Здесь же мы приведем описание функции dommappgoff() для
анонимных областей памяти.
Функция выполняет следующие действия:
1. Проверяет корректность значений параметров и возможность удовлетво-
удовлетворения запроса. В частности, функция проверяет наличие следующих пре-
препятствий для выполнения запроса:
• интервал линейных адресов имеет нулевую длину или включает в себя
адреса, большие, чем tasksize;
• процесс уже отобразил слишком много областей памяти. Иначе говоря,
значение в поле mapcount дескриптора памяти mm, принадлежащего
этому процессу, превышает максимальную допустимую величину;
• параметр flag показывает, что страницы нового интервала линейных
адресов должны быть заблокированы в оперативной памяти, а процессу
не разрешено создавать заблокированные области памяти, или количе-
количество страниц, заблокированных процессом, превышает порог, значение
КОТОрОГО ХраНИТСЯ В ПОЛе signal->rlim[RLIMIT_MEMLOCK] .rlim_cur ДеСК-
риптора процесса.
Если выполнено любое из этих условий, функция dommappgof f () закан-
заканчивает работу, возвращая отрицательное значение. Если интервал линей-
линейных адресов имеет нулевую длину, функция возвращает управление, не
выполнив никаких действий.
2. Вызывает функцию get_unmapped_area(), чтобы получить интервал линей-
ных адресов для новой области.
3. Вычисляет флаги новой области памяти, комбинируя значения параметров
prot И flags:
vm_flags = calc_vm_prot_bits(prot,flags) |
calc_vm_flag_bits(prot,flags) |
mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;
if (flags & MAP_SHARED)
vm_flags |= VM_SHARED | VM_MAYSHARE;
Функция caic_vrn_prot_bits() устанавливает флаги vm_read, vm_write и
vmexec в поле vmf lags, только если установлены соответствующие флаги
prot_read? prot_write и prot_exec в параметре prot. Функция
calc_vm_flag_bits() устанавливает флаги VM_GROWSDOWN, VM_DENYWRITE,
vm_executable и vm_locked в поле vm_f lags, только если установлены соот-
соответствующие флаГИ MAP_GROWSDOWN, MAPJDENYWRITE, MAP_EXECUTABLE И
maplocked в параметре flags. В поле vm_fiags устанавливается еще не-
несколько флагов: vm_mayread, vm_maywrite, vm_mayexec, флаги, устанавливае-
устанавливаемые по умолчанию для всех областей памяти и хранящиеся в поле
rnm->def_f lags5, а также оба флага vm_shared и vm_mayshare, если страницы
области памяти должны совместно использоваться другими процессами.
4. Вызывает функцию f indvmaprepare (), чтобы найти объект-область па-
памяти, который будет предшествовать новому интервалу, а также позицию
новой области в красно-черном дереве:
for (;;) {
vma = find_vma_prepare(ram, addr, &prev, &rb_link, &rb_parent);
if (!vma || vma->vm_start >= addr + len)
break;
if (do_munmap (mm, addr, len))
return -ENOMEM;
}
Кроме этого, функция find_vma_j>repare () проверяет, существует ли об-
область памяти, пересекающаяся с новым интервалом. Это имеет место, ко-
когда функция возвращает ненулевой адрес, указывающий на область, кото-
которая начинается до конца нового интервала. В таком случае функция
dommappgof f () вызывает domunmap (), чтобы удалить новый интервал и
повторяет шаг с начала (см. разд. "Освобождение интервала линейных ад-
адресов" далее в этой главе).
5. Проверяет, не приведет ли вставка новой области памяти к ситуации, в ко-
которой адресное пространство процесса (mm->total_vm<<PAGE_SHIFT)+len
превышает порог, хранящийся В ПОЛе signal->rlim[RLIMIT_AS] .rlim_cur
дескриптора процесса. Если проверка дает положительный результат,
функция возвращает код ошибки -enomem. Обратите внимание, что провер-
э В действительности, поле def_f lags дескриптора памяти изменяется только системным вызовом
mlockall(), которым можно воспользоваться для установки флага VMLOCKED и заблокировать,
таким образом, все будущие страницы вызывающего процесса в оперативной памяти.
ка производится на этом шаге, а не на шаге 1 одновременно с другими
проверками. Дело в том, что некоторые области памяти могли быть уда-
удалены на шаге 4.
6. Возвращает код ошибки -enomem, если флаг mapnoreserve не был уста-
установлен в параметре flags, новая область памяти содержит закрытые стра-
страницы, доступные для записи, а свободных страничных кадров недоста-
недостаточно. Эта заключительная проверка выполняется функцией security_vm_
enough__memory ().
7. Если новый интервал является закрытым (флаг vmshared не установлен)
и не отображает файл, хранящийся на диске, функция вызывает функцию
vmamergeo для проверки, может ли предшествующая область памяти
быть расширена так, чтобы она включала в себя новый интервал. Естест-
Естественно, предшествующая область должна иметь в точности те же флаги,
что хранятся в локальной переменной vmfiags. Если предшествующая
область памяти может быть расширена, функция vmamergeo одновре-
одновременно пытается слить ее со следующей областью (это происходит, когда
новый интервал заполняет "пустоту" между двумя областями памяти, и
все три области имеют одинаковые флаги). Если функции удастся расши-
расширить предшествующую область памяти, выполнение продолжится с ша-
шага 12.
8. Выделяет структуру vmareastruct для новой области памяти, вызывая
фуНКЦИЮ slab-аллокатора kmem_cache_alloc () .
9. Инициализирует новый объект-область памяти (на который указывает
vma):
vrna—>vm шт. = nun;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm flags = vm flags;
vma->vm_page_prot = protection_map[vm_flags & OxOf];
vma->vm ops = NULL;
vma->vm_pgoff = pgoff;
vma->vm_file = NULL;
vma->vm private data = NULL;
vma->vm_next = NULL;
INIT_LIST_HEAD(&vma->shared) ;
10. Если флаг mapshared установлен (и новая область памяти не отображает
файл, хранящийся на диске), значит, область является анонимной и со-
совместно используемой. Для ее инициализации функция вызывает функ-
функцию shmem zerosetupo. Анонимные совместно используемые области
применяются, в основном, для межпроцессного взаимодействия (см.
разд. "Совместно используемая память IP С" главы 19).
11. Вызывает функцию vmaiinko для занесения новой области в список об-
областей памяти и красно-черное дерево.
12. Увеличивает размер адресного пространства процесса в поле totaivm де-
дескриптора памяти.
13. Если флаг vmlocked установлен, функция вызывает функцию
makepagespresent (), чтобы выделить все страницы области памяти под-
подряд и заблокировать их:
if (vm_flags & VM_LOCKED) {
inm->locked_vm += len » PAGE_SHIFT;
make_pages_present(addr, addr + len);
}
ФуНКЦИЯ makepagespresent (), В СВОЮ очередь, ВЫЗЫВает функцию
get_user_pages():
write = (vma->vm_flags & VM_WRITE) != 0;
get_user_pages(current, current->mm, addr, len, write, 0, NULL, NULL);
Функция getuserpageso перебирает в цикле все начальные линейные
адреса страниц между addr и addr+ien. Для каждого из них она вызывает
функцию foiiowpage о, чтобы проверить, имеется ли отображение в фи-
физическую страницу в Таблицах Страниц процесса current. Если такой фи-
физической страницы нет, функция getuserpages() вызывает функцию
handiemmf auit (), которая, как мы увидим в разд. "Обработка ошибоч-
ошибочного адреса внутри адресного пространства", выделяет один странич-
страничный кадр и заполняет запись Таблицы Страниц в соответствии с полем
vm_f lags дескриптора области памяти.
14. В завершение своей работы функция возвращает линейный адрес новой
области памяти.
Освобождение интервала линейных адресов
Когда ядро должно удалить интервал линейных адресов из адресного про-
пространства текущего процесса, оно вызывает функцию domunmap(), которая
принимает следующие параметры: адрес mm дескриптора памяти процесса,
начальный адрес интервала start и его длину len. Интервал, подлежащий
удалению, обычно не соответствует какой-то конкретной области памяти;
он может находиться внутри одной области или охватывать несколько об-
областей.
Функция do_munmap()
Выполнение этой функции происходит в два этапа. На первом этапе (ша-
(шаги 1—6) она перебирает список областей памяти, принадлежащих процессу, и
отсоединяет все области, попадающие в интервал линейных адресов, от ад-
адресного пространства процесса. На втором этапе (шаги 7—12) функция об-
обновляет Таблицы Страниц процесса и удаляет области, определенные на пер-
первом Этапе. Функция пользуется функциями split_vma() И unmap_region(),
описанными далее. Функция domunmap () выполняет следующие действия:
1. Производит некоторые предварительные проверки значений параметров.
Если интервал линейных адресов включает в себя адреса, превышающие
tasksize, если значение start не кратно 4096, или если интервал имеет
нулевую длину, функция возвращает код ошибки -einval.
2. Находит первую область памяти mpnt, которая заканчивается после уда-
удаляемого интерфейса линейных адресов (mpnt->end > start), если таковая
существует:
mpnt = find_vma_prev(mm, start, &prev) ;
3. Если такой области нет, или найденная область не пересекается с интерва-
интервалом линейных адресов, ничего предпринимать не нужно, поскольку ин-
интервал не содержит ни одной области:
end = start + len;
if ('mpnt || mpnt->vm_start >= end)
return 0;
4. Если интервал линейных адресов начинается внутри области памяти mpnt,
функция вызывает функцию splitvmaO (описанную далее), чтобы раз-
разбить область на две меньшие: одну вне интервала, а другую — внутри
него:
if (start > mpnt->vm_start) {
if (split_vma(mm, mpnt, start, 0))
return -ENOMEM;
prev = mpnt;
}
Локальная переменная prev, которая до этого содержала указатель на об-
область памяти, предшествующую области mpnt, обновляется и теперь ука-
указывает на mpnt, т. е. на новую область памяти, лежащую вне интервала ли-
линейных адресов. Таким образом, переменная prev все равно указывает на
область памяти, предшествующую первой области, подлежащей уда-
удалению.
5. Если интервал линейных адресов заканчивается в пределах какой-либо
области памяти, функция вызывает функцию spiitvmaO еще раз, чтобы
разбить последнюю пересекающуюся с интервалом область на две мень-
меньшие: одну вне интервала, а другую — внутри него6:
last = f ind_vma (ram, end) ;
if (last && end > last->vm_start)){
if (split_vma(ram, last, start, end, 1))
return -ENOMEM;
}
6. Обновляет значение локальной переменной mpnt так, чтобы она указывала
на первую область памяти в интервале линейных адресов. Если перемен-
переменная prev содержит null, т. е. предшествующих областей нет, адрес первой
области памяти берется из поля пж->гатар:
mpnt = prev ? prev->vm_next : гат->гагаар;
7. Вызывает функцию detach_vraas_to_be_unraapped (), чтобы удалить области
памяти, попадающие в интервал линейных адресов, из адресного про-
пространства процесса. Эта функция, в сущности, выполняет следующий код:
vraa = mpnt;
insertion_point = (prev ? &prev->vm_next : &mra->mmap);
do {
rb_erase (&vma->vm_rb, &mm->ram_rb) ;
mrn->raap_count— ;
tail_vma = vma;
vma = vma->next;
} while (vma && vma->start < end);
*insertion_point = vma;
tail_vma->vm next = NULL;
ram->map_cache = NULL;
Дескрипторы областей, подлежащих удалению, хранятся в упорядоченном
списке, на голову которого указывает локальная переменная mpnt (этот
список фактически является фрагментом оригинального списка областей
памяти, принадлежащих процессу).
8. Получает СПИН-блокирОВКу mm->page_table_lock.
Если интервал целиком входит в область памяти, ее следует заменить на две меньшие области.
В этом случае на шагах 4 и 5 область памяти разбивается на три. Средняя область удаляется, а пер-
первая и последняя сохраняются.
9. Вызывает функцию unmapregion () (описанную далее) для очистки запи-
записей Таблицы Страниц, относящихся к интервалу линейных адресов, и ос-
освобождения соответствующих страничных кадров:
unmap_region(ram, mpnt, prev, start, end);
1 0. Освобождает СПИН-блОКИроВКу mm->page_table_lock.
11. Освобождает дескрипторы областей памяти, собранные в список на
шаге 7:
do {
struct vm_area struct * next = mpnt->vm_next;
unmap_vma (mm, mpnt) ;
mpnt = next;
} while (mpnt != NULL);
Функция unmap vma() вызывается для каждой области памяти в списке.
Она выполняет следующие действия:
• обновляет ПОЛЯ mm->total_vm И mm->locked_vm;
• выполняет метод mm->unmap_area дескриптора памяти. Этот метод реа-
реализован ОДНОЙ ИЗ функций, archunmaparea () ИЛИ arch_unmap_area_
topdowno, в зависимости от схемы расположения областей памяти
процесса. В любом случае обновляется поле mm->f reeareacache, если
это необходимо;
• вызывает метод close области памяти, если он определен;
• если область памяти анонимна, функция удаляет ее из списка аноним-
анонимных Областей, ГОЛОВа КОТОРОГО нахОДИТСЯ В ПОЛе mm->anon_vma;
• вызывает фуНКЦИЮ kmem_cache_f гее (), Чтобы ОСВОбодИТЬ деСКрИПТОр
области памяти.
12. Возвращает 0 (успешное завершение).
Функция split_vma()
Цель функции spiitvmao состоит в том, чтобы разбить область памяти, пе-
пересекающуюся с интервалом линейных адресов, на две меньшие, одну вне
интервала, а другую — внутри него. Функция принимает четыре параметра:
mm — указатель на дескриптор памяти; vma — указатель на дескриптор облас-
области памяти, который идентифицирует разбиваемую область; addr— адрес
точки пересечения интервала и области памяти; newbelow — флаг, который
показывает, произошло ли пересечение в начале или в конце интервала.
Функция выполняет следующие действия:
1. Вызывает функцию loriem_cache_alloc(), чтобы ПОЛучИТЬ дополнительный
дескриптор vmareastruct, и сохраняет его адрес в локальной переменной
new. Если свободной памяти нет, возвращает -enomem.
2. Инициализирует поля дескриптора new содержимым полей дескриптора
vma.
3. Если флаг newbelow сброшен, значит, интервал линейных адресов начина-
начинается внутри области vma, и, следовательно, новая область должна быть по-
помещена ПОСЛе области vma. ФунКЦИЯ записывает В ПОЛЯ new->vm_start И
vma->vm end значение параметра addr.
4. Если же флаг newbelow установлен, значит, интервал линейных адресов
заканчивается внутри области vma, и, следовательно, новая область должна
быть помещена перед областью vma. Поэтому функция записывает в поле
new->vm end И vma->vm_start значение параметра addr.
5. Если для новой области памяти определен метод open, функция выполняет
его.
6. ЗанОСИТ дескриптор области ПаМЯТИ new В СПИСОК Областей mm->mmap И В
красно-черное дерево mm->mm_rb. Кроме того, перестраивает дерево с уче-
учетом нового размера области памяти vma.
7. Возвращает 0 (успешное завершение).
Функция unmap_region()
Функция unmapregion () перебирает список областей памяти и освобождает
принадлежащие им страничные кадры. Она принимает пять параметров: mm —
указатель на дескриптор памяти; vma — указатель на дескриптор области па-
памяти, который идентифицирует первую удаляемую область памяти; prev —
указатель на область памяти, предшествующую области vma в списке облас-
областей процесса (шаги 2 и 3 в описании функции domunmap ()); start и end —
адреса, ограничивающие удаляемый интервал линейных адресов. Функция
выполняет следующие действия:
1. Вызывает функцию iru_add_drain() (см. главу 17).
2. Вызывает функцию tibgathermmuo, чтобы проинициализировать пере-
переменную mmugathers (свою у каждого процессора). Содержимое этой пе-
переменной зависит от архитектуры. Вообще говоря, переменная
mmugathers должна хранить всю информацию, необходимую для успеш-
успешного обновления записей таблиц страниц процесса. В архитектуре 80x86
функция tibgathermmuo просто сохраняет значение указателя на деск-
дескриптор памяти mm в переменной mmugathers локального процессора.
3. Сохраняет адрес переменной mmugathers в локальной переменной tib.
4. Вызывает функцию unmapvmaso для просмотра всех записей Таблицы
Страниц, соответствующих интервалу линейных адресов. При наличии
только одного процессора функция многократно вызывает функцию
f reeswapandcache (), чтобы ОСВОбоДИТЬ Нужные Страницы (см. главу 17).
В противном случае функция сохраняет указатели на дескрипторы стра-
страниц В локальной переменной mmu_gathers.
5. Вызывает функцию free_pgtables (tlb,prev, start, end), пытаясь утиЛИЗИ-
ровать Таблицы Страниц процесса, очищенные на предыдущем шаге.
6. Вызывает функцию tib_finish_mmu(tib, start, end) для завершения рабо-
работы. Вызванная функция:
• вызывает функцию flushtibmmO для очистки TLB-буфера;
• В многопроцессорных Системах Вызывает фуНКЦИЮ free_pages_and_
swapcache (), чтобы освободить страничные кадры, указатели на кото-
которые собраны в структуре immugather. Эта функция описана в главе 17.
Обработчик исключения
"ошибка обращения к странице"
Как было сказано ранее, обработчик исключения "ошибка обращения к стра-
странице" в Linux должен отличать исключения, сгенерированные программными
ошибками от тех, что вызваны попытками обращения к странице, действи-
действительно принадлежащей адресному пространству процесса, но просто еще не
выделенной.
Дескрипторы областей памяти позволяют обработчику исключения эффек-
эффективно выполнять свою работу. Функция dopagef auit (), являющаяся слу-
служебной процедурой прерывания "ошибка обращения к странице" в архитек-
архитектуре 80x86, сравнивает линейный адрес, послуживший причиной возникно-
возникновения ошибки, с адресами областей памяти процесса current. Таким образом,
она может определить верный способ обработки исключения в соответствии
со схемой, изображенной на рис. 9.4.
На практике все намного сложнее, потому что обработчик этого исключения
должен распознавать несколько конкретных случаев, плохо вписывающихся
в общую схему. Кроме того, он должен различать несколько видов легальных
попыток доступа. Подробная блок-схема обработчика изображена на рис. 9.5.
Идентификаторы vmalloc_fault, good_area, bad_area И no_context ЯВЛЯЮТСЯ
метками в коде функции dopagef auit (). Они позволяют читателю соотне-
соотнести элементы блок-схемы с конкретными строчками кода.
Рис. 9.4. Общая схема обработки ошибки обращения к странице
Функция dopagef auit () принимает следующие входные параметры:
□ regs — адрес структуры ptregs, в которой хранится содержимое регист-
регистров микропроцессора на момент возникновения исключения;
□ errorcode — трехбитовый код ошибки, заносимый в стек блоком управ-
управления в момент возникновения исключения (см. главу 4) \ биты несут сле-
следующую информацию:
• если бит 0 сброшен, значит, исключение вызвано попыткой обращения
к отсутствующей странице (флаг Present в записи Таблицы Страниц
сброшен); если же бит 0 установлен, значит, исключение вызвано несо-
несоответствием прав доступа;
• если бит 1 сброшен, значит, исключение вызвано попыткой чтения или
выполнения; если он установлен, значит, исключение вызвано попыт-
попыткой записи;
• если бит 2 сброшен, значит, исключение возникло, когда процессор ра-
работал в режиме ядра; в противном случае оно возникло в режиме поль-
пользователя.
Первой операцией функции dopagef auit () является чтение линейного адре-
адреса, вызвавшего исключение. Когда оно произошло, управляющий блок про-
процессора сохранил это значение в регистре сг2:
asm("movl %%cr2,%0":"=r" (address));
if (regs->eflags & 0x00020200)
local_irq_enable();
tsk = current;
Прочитанный линейный адрес сохраняется в локальной переменной address.
Кроме того, функция обеспечивает включение локальных прерываний, если
они были включены до возникновения исключения, или если процессор ра-
работал в режиме virtual-8086, и сохраняет указатели на дескриптор текущего
процесса в локальной переменной tsk.
Рис. 9.5. Блок-схема обработчика исключения "ошибка обращения к странице"
Как видно из верхней части рис. 9.5, функция dopagefauit о проверяет,
принадлежит ли "ошибочный" линейный адрес четвертому гигабайту:
info.si_code = SEGV_MAPERR;
if (address >= TASK_SIZE ) {
if (!(error_code & 0x101))
goto vmalloc fault;
goto bad_area_nosemaphore;
}
Если исключение вызвано попыткой ядра обратиться к несуществующему
страничному кадру, выполняется переход по метке vmaiiocfauit на фраг-
фрагмент кода, обрабатывающий ошибки, вероятно, вызванные обращением к не-
несмежной области памяти в режиме ядра. В противном случае делается пере-
переход по метке badareanosemaphore на фрагмент кода, который обсуждается
далее в этой главе.
Затем обработчик исключения проверяет, возникло ли оно во время выпол-
выполнения ядром какой-нибудь критической процедуры или при работе какого-
либо потока ядра (вспомним, что поле mm дескриптора процесса содержит
null для потоков ядра):
if (in_atomic () || !tsk->iran)
goto bad area nosemaphore;
Макрос inatomic () возвращает единицу, если исключение возникло в одной
из следующих ситуаций:
□ ядро выполняло какой-либо обработчик прерываний или функцию отло-
отложенного выполнения;
□ ядро работало с критической областью при выключенном вытеснении
в ядре (см. главу 5).
Если ошибка обращения к странице связана с обработчиком прерываний,
функцией отложенного выполнения, критической областью или потоком яд-
ядра, функция dopagef auit () не пытается сравнить линейный адрес с облас-
областями памяти процесса current. Потоки ядра никогда не обращаются к линей-
линейным адресам ниже tasksize. Обработчикам прерываний, функциям отло-
отложенного выполнения, а также коду критических областей тоже не следует
обращаться к линейным адресам ниже tasksize, поскольку это может бло-
блокировать текущий процесс (далее в этой главе обсуждается локальная пере-
переменная info И фрагмент КОДа за меткой badareanosemaphore).
Предположим, что ошибка обращения к странице не связана ни с обработчи-
обработчиком прерывания, ни с функцией отложенного выполнения, ни с критической
областью, ни с потоком ядра. Тогда функция должна исследовать области
памяти, принадлежащие процессу, чтобы определить, находится ли адрес,
вызвавший ошибку, в адресном пространстве процесса. Для этой цели она
должна получить mmapsem, семафор процесса для чтения/записи:
if (!down_read_trylock(&tsk->imi->nimap_sem) ) {
if ((error_code & 4) == 0 &&
!search_exception_table(regs->eip))
goto bad_area_nosemaphore;
down_read (&tsk->mm->mmap_sem) ;
}
Если ошибки ядра и сбои аппаратуры исключены, то к моменту возникнове-
возникновения ошибки обращения к странице текущий процесс еще не получил семафор
mmapsem для записи. Тем не менее функция dopagef auit () должна быть
уверена, что это действительно так, иначе возникнет взаимная блокировка.
По этой причине функция вызывает функцию downreadtryiocko, а не
downreado (см. главу 5). Если семафор закрыт, а ошибка обращения к стра-
странице произошла в режиме ядра, функция dopagef auit () проверяет, не воз-
возникло ли исключение из-за того, что некий линейный адрес был передан ядру
в качестве параметра системного вызова. В этом случае функция
dopagef auit () знает наверняка, что семафором владеет другой процесс, по-
поскольку любая служебная процедура системного вызова аккуратно избегает
получения семафора mmapsem для записи до обращения к адресному про-
пространству режима пользователя. Поэтому функция ждет, когда семафор бу-
будет освобожден. В противном случае причиной исключения является про-
программная ошибка ядра или серьезная аппаратная проблема, и функция пере-
переходит на метку bad_area_nosemaphore.
Предположим, что семафор был корректно получен для чтения. Тогда функ-
функция mmapsem ищет область памяти, содержащую адрес, вызвавший ошибку:
vma = find_vma (tsk->ram, address);
if (!vma)
goto bad_area;
if (vma->vm_start <= address)
goto good_area;
Если переменная vma содержит null, to нет областей памяти, заканчиваю-
заканчивающихся после адреса address, и, следовательно, указан действительно ошибоч-
ошибочный адрес. С другой стороны, если первая область памяти, заканчивающаяся
после адреса address, содержит его, функция переходит на фрагмент кода
С меткой good_area.
Если ни одно из условий if не выполняется, можно сделать вывод, что адрес
address не входит ни в одну область памяти. Однако функция должна произ-
произвести дополнительную проверку, поскольку ошибка обращения к странице
может быть вызвана инструкциями push или pusha, действующими на стек
режима пользователя.
Здесь мы сделаем небольшое отступление, чтобы объяснить, как стеки ото-
отображаются в области памяти. Каждая область, содержащая стек, расширяется
в направлении нижних адресов. Ее флаг vmgrowsdown установлен, и значение
поля vmend остается фиксированным, в то время как значение поля vmstart
может быть уменьшено. Область памяти заключает в себе стек режима поль-
пользователя, но точное соответствие ее границ размеру стека не имеет места.
Причины такого расхождения следующие:
□ размер области памяти 4 Кбайт (она должна содержать целое число стра-
страниц), а размер стека произволен;
□ страничные кадры, присваиваемые области, не освобождаются, пока вся
область не будет удалена; в частности, значение поля vm_start области,
содержащей стек, может только уменьшаться и никогда не увеличивается.
Даже если процесс выполнит несколько инструкций pop подряд, размер
области останется прежним.
Теперь читателю должно быть ясно, как процесс, заполнивший последний
страничный кадр, выделенный его стеку, может вызвать исключение "ошибка
обращения к странице": инструкция push ссылается на адрес за пределами
области памяти (и на несуществующий страничный кадр). Обратите внима-
внимание, что этот вид исключения не вызван ошибкой в программе и обработчик
должен отреагировать на него специальным образом.
Вернемся к описанию функции do_page_fauit (), которая проверяет ситуа-
ситуацию, описанную ранее:
if (!(vma->vm_flags & VM_GROWSDOWN))
goto bad_area;
if (error_code & 4 /* User Mode */
&& address + 32 < regs->esp)
goto bad_area;
if (expand_stack(vma, address))
goto bad_area;
goto good_area;
Если флаг vmgrowsdown этой области установлен, а исключение возникло в
режиме пользователя, функция убеждается, что адрес address меньше, чем
указатель стека regs->esp (он должен быть ненамного меньше). Поскольку
некоторые ассемблерные инструкции для работы со стеком (например, pusha)
уменьшают содержимое регистра esp только после обращения к памяти, про-
процессу дается 32-байтовый интервал допуска. Если адрес расположен доста-
достаточно высоко (в пределах указанного допуска), код вызывает функцию
expandstack () для проверки, разрешено ли процессу расширять свой стек и
свое адресное пространство. Если все в порядке, функция записывает в поле
vmstart дескриптора vma значение address и возвращает 0; в противном слу-
случае она возвращает -enomem.
Обратите внимание, что код, приведенный ранее, обходит проверку допуска,
когда флаг vmgrowsdown установлен, но исключение возникло не в режиме
пользователя. Эти условия означают, что ядро обращается к стеку режима
пользователя, и что код должен всегда вызывать функцию expandstack ().
Обработка ошибочного адреса,
не входящего в адресное пространство
Если адрес address не принадлежит адресному пространству процесса, функ-
функция dopagef auit () выполняет операторы после метки badarea. Если ошиб-
ошибка возникла в режиме пользователя, функция посылает текущему процессу
сигнал sigsegv (см. разд. "Генерирование сигнала" главы 11) и завершает ра-
работу:
bad_area:
up_read (&tsk->mm->mmap_sem) ;
bad area_nosemaphore:
if (error_code & 4) { /* User Mode */
tsk->thread.cr2 = address;
tsk->thread.error_code = error_code | (address >= TASK_SIZE) ;
tsk->thread.trap_no = 14;
info.si_signo = SIGSEGV;
info.si_errno = 0;
info.si_addr = (void *) address;
force_sig_infо(SIGSEGV, &info, tsk);
return;
}
Функция forcesiginfoo обеспечивает, что процесс не проигнорирует и не
заблокирует сигнал sigsegv, и отправляет сигнал процессу режима пользова-
пользователя, одновременно передавая некоторую дополнительную информацию в
локальной переменной info. Поле info.sicode уже содержит значение
segvmaperr (если исключение было вызвано обращением к несуществующе-
несуществующему страничному кадру) или segvaccerr (если исключение было вызвано не-
недопустимым обращением к существующему страничному кадру).
Если исключение произошло в режиме ядра (бит2 параметра errorcode
сброшен), остается два возможных случая:
□ исключение возникло из-за линейного адреса, переданного ядру в качестве
параметра системного вызова;
□ исключение возникло из-за ошибки в ядре.
Функция различает эти ситуации следующим образом:
no_context:
if ((fixup = search exception table(regs->eip)) != 0) {
regs->eip = fixup;
return;
}
В первом случае выполняется "код обработки", который обычно сводится к
отправке сигнала sigsegv процессу current или к завершению обработчика
системного вызова с соответствующим кодом ошибки (см. разд. "Динами-
"Динамическая проверка адресов: код обработки исключения" главы 10).
Во втором случае функция выводит полный дамп регистров процессора и
стека режима ядра на консоль и в буфер системных сообщений. После этого
она уничтожает текущий процесс с помощью функции doexito (см. гла-
главу 20). Это так называемая ошибка "Kernel oops". Выведенные сообщения мо-
могут быть использованы специалистами для реконструкции ситуации и поиска
и исправления ошибки.
Обработка ошибочного адреса,
входящего в адресное пространство
Если адрес address принадлежит адресному пространству процесса, функция
dopagef ault () выполняет операторы после метки good_area:
good_area:
info.si_code = SEGV_ACCERR;
write = 0;
if (error_code & 2) { /* write access */
if (!(vma->vm_flags & VM_WRITE))
goto bad_area;
write++;
} else /* read access */
if ((error_code & 1) I| !(vma->vm_flags & (VM_READ | VM_EXEC)))
goto bad_area;
Если исключение было вызвано попыткой записи, функция проверяет, дос-
доступна ли область для записи. Если нет, выполняется код за меткой badarea;
если область доступна для записи, функция записывает 1 в локальную пере-
переменную write.
Если исключение было вызвано попыткой чтения или выполнения, функция
проверяет, присутствует ли страница в оперативной памяти. В этом случае
исключение было сгенерировано, потому что процесс попытался обратиться
к привилегированному страничному кадру (у которого сброшен флаг
user/Supervisor) в режиме пользователя, и поэтому функция переходит на
метку badarea7. Если страница отсутствует, функция также проверяет, дос-
доступна ли область памяти для чтения или выполнения.
Если права доступа к области памяти не конфликтуют с типом обращения,
вызвавшего исключение, вызывается функция handie_mm_fauit (), которая
выделяет новый страничный кадр:
survive:
ret = handle_mm_fault(tsk->mm, vma, address, write);
if (ret == VM_FAULT_MINOR || ret == VM_FAULT_MAJOR) {
if (ret == VM_FAULT_MINOR) tsk->min_flt++; else tsk->maj_flt++;
up_read (&tsk->mm->rnmap_sem) ;
return;
}
Функция handie_rrm_f auit () возвращает значение vm_fault_minor или
vmfaultmajor, если ей удалось выделить процессу новый страничный кадр.
Значение vmfaultminor является индикатором того, что ошибка обращения
к странице была обработана без блокирования текущего процесса; этот тип
ошибки называется незначительной ошибкой. Значение vmfaultmajor сви-
свидетельствует о том, что ошибка привела к блокированию текущего процесса
(вероятнее всего, потому что потребовалось определенное время на заполне-
заполнение страничного кадра, выделенного процессу, данными, прочитанными с
диска). Ошибка обращения к странице, приведшая к блокированию текущего
процесса, называется значительной ошибкой. Кроме того, функция может
возвратить значение vmfaultoom (недостаточно памяти) или vmfaultsigbus
(любая другая ошибка).
Если функция handie_mm_fauit () возвращает значение vm_fault_sigbus, про-
процессу отправляется сигнал sigbus:
if (ret == VM_FAULT_SIGBUS) {
do_sigbus:
up_read (&tsk->mm->mmap_sem) ;
if (!(error_code & 4) ) /* Kernel Mode */
goto no_context;
tsk->thread.cr2 = address;
tsk->thread.error_code = error_code;
tsk->thread.trap no = 14;
7 Впрочем, это никогда не произойдет. Ядро не присваивает процессам привилегированные стра-
страничные кадры.
info.si_signo = SIGBUS;
info.si_errno = 0;
info.si_code = BUS_ADRERR;
info.si_addr = (void *) address;
force_sig_infо(SIGBUS, &info, tsk);
}
Если функция handieiranf auit () не может выделить новый страничный кадр,
она возвращает значение vmfaultoom. В этом случае ядро обычно уничтожа-
уничтожает текущий процесс. Однако, если текущим является процесс init, он ставит-
ставится в конец очереди на выполнение, и вызывается планировщик. Когда про-
процесс init ВОЗОбнОВИТ работу, будет СНОВа ВЫЗВана фуНКЦИЯ handle_
ram_fault():
if (ret = VM_FAULT_OOM) {
out_of _memory:
up_read(&tsk->rran->ramap_sem) ;
if (tsk->pid != 1) {
if (error_code & 4) /* User Mode */
do_exit(SIGKILL);
goto no_context;
}
yield () ;
down_read(&tsk->inm->mmap sem) ;
goto survive;
}
Функция handle mmf ault () принимает четыре параметра:
□ mm— указатель на дескриптор памяти процесса, выполнявшегося, когда
возникло исключение;
□ vma— указатель на дескриптор области памяти, содержащий линейный
адрес, который вызвал исключение;
□ address — линейный адрес, который вызвал исключение;
□ writeaccess — единица, если программа tsk пыталась произвести запись
по адресу address, и ноль, если имела место попытка чтения или выполне-
выполнения.
Функция начинается с проверки существования среднего каталога страниц и
Таблицы Страниц, используемых для отображения адреса address. Даже если
адрес принадлежит адресному пространству процесса, необходимые Таблицы
Страниц могут быть еще не выделены, так что задача их выделения становит-
становится первоочередной:
pgd = pgd_offset(mm, address);
spin_lock (&mm->page_table_lock) ;
pud = pud alloc(mm, pgd, address);
if (pud) {
pmd = pmd_alloc(mm, pud, address);
if (pmd) {
pte = pte_alloc_map(mm, pmd, address);
if (pte)
return handle_pte_fault(mm, vma, address,
write_access, pte, pmd);
}
}
spin_unlock(&mm->page_table_lock);
return VM_FAULT_OOM;
Локальная переменная pgd содержит запись глобального каталога страниц,
ссылающуюся на адрес address. В случае необходимости вызываются функ-
функции pudaiioco и pmdaiioco для выделения нового верхнего каталога стра-
страниц и нового среднего каталога страниц соответственно .
Затем, опять же, при необходимости, вызывается функция pteaiiocmapo
для выделения новой Таблицы Страниц. Если обе операции прошли успешно,
локальная переменная pte указывает на запись в Таблице Страниц, ссылаю-
ссылающуюся на адрес address. Далее вызывается функция handle_pte_f ault () ДЛЯ
изучения записи Таблицы Страниц, соответствующей адресу address, и выяс-
выяснения способа выделения нового страничного кадра процессу:
□ если страница, к которой произошло обращение, отсутствует (то есть еще
не находится ни в одном страничном кадре), ядро выделяет новый стра-
страничный кадр и инициализирует его, как полагается; этот способ называет-
называется выделением страниц по требованию;
□ если страница, к которой произошло обращение, присутствует, но доступ-
доступна только для чтения (то есть уже находится в каком-то страничном кад-
кадре), ядро выделяет новый страничный кадр и инициализирует его, копируя
данные из старого страничного кадра; этот способ называется копирова-
копированием при записи.
Выделение страниц по требованию
Термин выделение страниц по требованию относится к способу выделения
динамической памяти, который состоит в том, чтобы отложить выделение
8 В процессорах 80><86 это не происходит, потому что верхние каталоги страниц всегда входят в
состав глобального каталога страниц, а средние каталоги страниц либо содержатся в верхнем (меха-
(механизм РАЕ отключен), либо выделяются вместе с ним (механизм РАЕ включен).
страничного кадра до последнего момента, т. е. до попытки процесса обра-
обратиться к странице, отсутствующей в памяти, и, следовательно, до возникно-
возникновения исключения "ошибка обращения к странице".
В основе такого подхода лежит тот факт, что процессы не обращаются ко
всем адресам своего адресного пространства сразу после начала работы. На
практике некоторые из этих адресов могут так и не понадобиться процессу.
Кроме того, принцип компактности программ (см. главу 2) гарантирует, что
на любом этапе выполнения программы только небольшое подмножество
страниц процесса реально востребовано, и поэтому страничные кадры, со-
содержащие временно не нужные страницы, могут быть использованы другими
процессами. Выделение страниц по требованию предпочтительнее, чем гло-
глобальное выделение (при котором все страничные кадры выделяются процессу
с самого начала и остаются в памяти до его завершения), потому что увели-
увеличивает среднее количество свободных страничных кадров в системе и таким
образом обеспечивает более эффективное использование свободной памяти.
С другой точки зрения, оно позволяет системе в целом рациональнее обра-
обращаться с конкретным объемом памяти, имеющейся в наличии.
Платой за эти положительные моменты является повышение накладных рас-
расходов системы. Каждое исключение "ошибка обращения к странице", вы-
вызванное этим подходом, должно быть обработано ядром, на что уходят циклы
центрального процессора. К счастью, принцип компактности обеспечивает,
что процесс, обратившийся к группе страниц, определенное время работает
только с ними и не обращается к другим. Таким образом, данное исключение
можно считать редким событием.
Страница, к которой обратился процесс, может отсутствовать в основной па-
памяти, либо потому что процесс к ней до сих пор не обращался, либо потому
что соответствующий страничный кадр был утилизирован ядром (см. гла-
главу 17).
В обоих случаях обработчик исключения должен присвоить процессу новый
страничный кадр. Однако способ инициализации этого страничного кадра
зависит от типа страницы и от того, обращался ли процесс к ней ранее. В ча-
частности:
1. Либо процесс никогда не обращался к странице, и она не отображает
файл, расположенный на диске, либо страница отображает файл. Ядро
распознает эти случаи, потому что запись Таблицы Страниц заполнена ну-
нулями, т. е. макрос pte none возвращает значение 1.
2. Страница принадлежит нелинейному отображению файла (см. разд. "Не-
"Нелинейные отображения в память" главы 16). Ядро распознает этот слу-
случай, поскольку флаг Present сброшен, а флаг Dirty установлен, т. е. макрос
ptefile возвращает значение 1.
3. Процесс уже обращался к странице, но ее содержимое временно сохране-
сохранено на диске. Ядро распознает этот случай по нулевой записи в Таблице
Страниц И сброшенным флагам Present И Dirty.
Таким образом, функция handie_pte_f auit () способна различить эти три слу-
случая, изучая запись Таблицы Страниц, ссылающуюся на адрес address:
entry = *pte;
if (!pte_present(entry)) {
if (pte_none(entry))
return do_no_page(mm, vma, address, write_access, pte, prod);
if (pte_file(entry))
return do_file_page(mm, vma, address, write_access, pte, pmd) ;
return do swap page(mm, vma, address, pte, pmd, entry, write_access) ;
}
Случаи 2 и З мы рассмотрим, соответственно, в главах 16 и 17.
8 первом случае, когда процесс прежде не обращался к странице, или стра-
страница линейно отображает файл, вызывается функция donopage (). Сущест-
Существует два способа загрузить недостающую страницу, в зависимости от того,
отображена ли она в файл на диске. Функция определяет это, проверяя метод
nopage области памяти vma. Этот метод указывает на функцию, которая за-
загружает недостающую страницу с диска в оперативную память, если она бы-
была отображена в файл. Таким образом, существуют две возможности:
□ поле vma->vm_ops->nopage не равно null. В этом случае область памяти
отображает файл, а поле указывает на функцию, которая загружает стра-
страницу. Такая ситуация обсуждается в разд. "Выделение страниц по требо-
требованию для отображения в память" главы 16 и разд. "Совместно исполь-
используемая память IP С" главы 19;
□ Либо ПОЛе vma->vm_ops, либо ПОЛе vma->vm_ops->nopage СОДерЖИТ NULL.
В этом случае область памяти не отображает файл, т. е. является
анонимным отображением. Функция do_no_page() вызывает функцию
doanonymouspage (), чтобы ПОЛуЧИТЬ НОВЫЙ СТранИЧНЫЙ кадр:
if (!vma->vm_ops || !vma->vm_ops->nopage)
return do_anonymous_page(mm, vma, page_table, pmd,
write_access, address);
Функция doanonymouspage ()9 обрабатывает запросы на запись и на чтение
раздельно:
9 Для упрощения описания этой функции мы опускаем операторы, относящиеся к обратному ото-
отображению.
if (write_access) {
pte_unmap(page_table);
spin_unlock (&ram->page_table_lock) ;
page = alloc_page(GFP_HIGHUSER | GFP_ZERO);
spin lock(&mm->page table lock);
page_table = pte_offset_map(pmd/ addr);
mm->rss++;
entry = maybe_mkwrite(pte_mkdirty(mk_pte(page,
vma->vm_page_prot) ) , vma) ;
lru_cache_add_active(page);
SetPageReferenced(page);
set_pte (page__table, entry);
pte_unmap(page_table);
spin_unlock(&ram->page_table_lock);
return VM_FAULT_MINOR;
}
При первом выполнении макроса pteunmap освобождается временное ото-
отображение ядра для верхнего физического адреса записи Таблицы Страниц,
установленное макросом pte_offset_map ДО ВЫЗОВа функции handle_pte_
fault о (см. табл. 2.7). Следующая пара макросов pte_offset_map и pte_unmap
получит и освободит то же самое временное отображение ядра. Это отобра-
отображение должно быть освобождено перед вызовом функции aiiocpageO, по-
потому что данная функция может блокировать текущий процесс.
Функция увеличивает значение в поле rss дескриптора памяти, чтобы можно
было отследить количество страничных кадров, выделенных процессу.
В запись Таблицы Страниц заносится физический адрес страничного кадра,
который помечен как доступный для записи10 и "грязный". Функция
iru_cache_add_active() вставляет новый страничный кадр в структуры,
имеющие отношение к выгрузке на диск. Эта тема обсуждается в главе 17.
Если же был сделан запрос на чтение, то содержимое страницы роли не игра-
играет, потому что процесс обращается к ней впервые. Безопаснее предоставить
процессу страницу, заполненную нулями, чем старую страницу с информаци-
информацией, записанной другими процессами. Linux идет еще дальше, следуя духу вы-
выделения страниц по требованию. Нет необходимости присваивать процессу
новый страничный кадр с нулями прямо сейчас, поскольку с тем же успехом
можно предоставить ему заранее подготовленную так называемую нулевую
страницу и отложить выделение страничного кадра еще на какое-то время.
10 Если отладчик попытается записать что-то на страницу, принадлежащую области памяти отлажи-
отлаживаемого процесса, доступной только для чтения, ядро не будет устанавливать флаг Read/Write.
Этот специальный случай обрабатывается функцией maybejnkwrite ().
Нулевая страница выделяется статически во время инициализации ядра в пе-
переменной ernptyzeropage (являющейся массивом из 4096 байтов, заполнен-
заполненных нулями).
Итак, в запись Таблицы Страниц заносится физический адрес нулевой стра-
страницы:
entry = pte_wrprotect (mk_pte (virt_to_page (empty_zero_page) ,
vma->vm_page_prot) ) ;
set_pte(page_table, entry);
spin unlock(&mm->page table_lock);
return VM_FAULT_MINOR;
Поскольку эта страница помечена как недоступная для записи, если процесс
попытается записать в нее данные, то сработает механизм копирования для
записи. Лишь после этого процесс получит, наконец, собственную страницу
для записи в нее. Этот механизм описан в следующем разделе.
Копирование при записи
В системах Unix первых поколений создание процесса было реализовано до-
довольно неуклюже: получив системный вызов fork о, ядро в буквальном
смысле дублировало все адресное пространство родителя и присваивало ко-
копию процессу-потомку. Такая операция занимало очень много времени, по-
поскольку для нее требовалось:
□ выделение страничных кадров под Таблицы Страниц процесса-потомка;
□ выделение страничных кадров под страницы процесса-потомка;
□ инициализация Таблиц Страниц процесса-потомка;
□ копирование страниц родителя в соответствующие страницы потомка.
Такой способ создания адресного пространства включал много обращений к
памяти, использовал много рабочих циклов процессора и существенно "пор-
"портил" содержимое кэша. Кроме всего прочего, это часто не имело никакого
смысла, потому что многие процессы-потомки начинают свою работу с за-
загрузки новой программы и полностью отказываются от унаследованного ад-
адресного пространства (см. главу 20).
Современные ядра Unix, в том числе ядро Linux, предпринимают более эф-
эффективный подход, называемый копированием при записи. Идея достаточно
проста: страничные кадры не копируются, а используются совместно родите-
родителем и потомком. Однако при совместном использовании их нельзя модифи-
модифицировать. Как только родитель или потомок попытается записать данные в
совместно используемый страничный кадр, возникнет исключение. В этот
момент ядро скопирует страницу в новый страничный кадр и пометит его как
доступный для чтения. Оригинальный страничный кадр остается недоступ-
недоступным для записи. Когда другой процесс попытается что-то записать в него,
ядро проверит, является ли пишущий процесс единственным владельцем
страничного кадра. Если является, оно сделает страничный кадр доступным
для записи для этого процесса.
Поле count дескриптора страницы служит для отслеживания количества
процессов, совместно использующих страничный кадр. Когда какой-то про-
процесс освобождает страничный кадр или над кадром выполняется операция
копирования при записи, значение в поле count этого кадра уменьшается.
Страничный кадр освобождается, только когда значение count сравняется
с-1 (см. главу 8).
Опишем, как копирование для записи реализовано в Linux. Когда функция
handieptef auit () распознает, что исключение "ошибка обращения к стра-
странице" было вызвано обращением к странице, присутствующей в памяти, она
выполняет следующий фрагмент кода:
if (pte_present(entry)) {
if (write_access) {
if (!pte_write(entry))
return do_wp_page(ram, vma, address, pte, pmd, entry);
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);
set_pte(pte, entry);
flush_tlb_page(vma, address);
pte_unraap(pte);
spin_unlock (&mm->page_table_lock) ;
return VM_FAULT_MINOR;
}
Функция handieptef auit () является архитектурно-независимой; она рас-
рассматривает все возможные нарушения прав доступа к страницам. Однако в
архитектуре 80x86 , если страница находится в памяти, значит, имела место
попытка записи, а страничный кадр для записи недоступен. Таким образом,
функция do wppage () вызывается всегда.
Функция dowppageo11 начинает с получения дескриптора страницы того
страничного кадра, на который ссылается запись Таблицы Страниц, вовле-
вовлеченная в обработку исключения. Затем функция определяет, действительно
11 Для упрощения описания этой функции мы опускаем операторы, относящиеся к обратному ото-
отображению.
ли необходимо копировать страницу. Если страницей владеет только один
процесс, копирование при записи не применяется, и процесс волен записы-
записывать данные на страницу. Грубо говоря, функция читает поле count дескрип-
дескриптора страницы: если оно содержит 0 (единственный владелец), копирование
при записи не производится. Впрочем, на практике эта проверка немного
сложнее, потому что поле count увеличивается также, когда страница зано-
заносится в кэш выгрузки (см. разд. "Кэш выгрузки" главы 17), и когда
устанавливается флаг PGprivate. Однако когда копирование при записи
делать не нужно, страничный кадр помечается как доступный для записи, и
при следующих попытках записать в него исключение "ошибка обращения к
странице" не возникает:
setjpte(page_table, maybe_mkwrite(pte_mkyoung(pte_mkdirty(pte)),vma));
flush_tlb_page(vma, address);
pte_unmap(page_table);
spin unlock(&mm->page table_lock);
return VM_FAULT_MINOR;
Если страница совместно используется несколькими процессами с примене-
применением техники копирования при записи, функция копирует содержимое старо-
старого страничного кадра (oidpage) в только что выделенный (newpage). Во из-
избежание конфликтов параллельного обращения вызывается функция
getpage (), КОТОрая увеличивает СЧетчИК ССЫЛОК СТраниЧНОГО кадра oidpage
до начала копирования:
old_page = pte_page(pte);
pte_unmap(page_table);
get_page(old_page);
spin_unlock (&mm->page_table_lock) ;
if (old_page == virt_to_page(empty_zero_page))
new_page = alloc_page(GFP_HIGHUSER | GFP_ZERO);
} else {
new_page = alloc_page(GFP_HIGHUSER);
vfrom = kmap_atomic (old_page, KMJJSERO)
vto = kmap_atomic (new_page, KM_USER1) ;
copy_page(vto, vfrom);
kunmap_atomic (vf rom, KM_USER0) ;
kunmap_atomic (vto, KM_USER0) ;
}
Если старая страница является нулевой, новый кадр при выделении заполня-
заполняется нулями (флаг gfpzero). В противном случае содержимое страничного
кадра копируется с помощью макроса сорураде (). Строго говоря, отдельная
обработка случая нулевой страницы не является необходимой, но она повы-
шает производительность системы, потому что оберегает аппаратный кэш
микропроцессора тем, что делает меньше обращений к памяти.
Поскольку выделение страничного кадра может блокировать процесс, функ-
функция проверяет, была ли модифицирована запись Таблицы Страниц после на-
начала выполнения функции (равны ЛИ Значения pte И *page_table). В ЭТОМ
случае новый страничный кадр освобождается, счетчик ссылок кадра
oldpage уменьшается (для отмены ранее сделанного увеличения), и функция
завершает работу.
Если все выглядит нормально, физический адрес нового страничного кадра,
в конце концов, заносится в запись Таблицы Страниц, и содержимое соответ-
соответствующего регистра TLB становится недействительным:
spin_lock (&mm->page_table_lock) ;
entry = maybejnkwrite (ptejnkdirty (mk_pte (new_page,
vma->vm_page_prot) ) ,vma) ;
set_pte(page_table, entry);
flush_tlb_page(vma, address);
lru_cache_add_active(new_page);
pte_unmap(page_table);
spin_unlock (&rnm->page_table_lock) ;
ФуНКЦИЯ lru_cache_add_active() ЗаНОСИТ НОВЫЙ СТраНИЧНЫЙ Кадр В СТруКТу-
ры, имеющие отношение к выгрузке на диск; ее описание приведено в гла-
главе 17.
Наконец, функция dowppageo дважды уменьшает счетчик ссылок кадра
oldpage. Первое уменьшение отменяет увеличение, сделанное в целях безо-
безопасности перед копированием содержимого страничного кадра, а второе от-
отражает тот факт, что текущий процесс больше не владеет этим страничным
кадром.
Обработка обращений
к несмежным областям памяти
В главе 8 мы видели, что ядро весьма неохотно обновляет записи Таблицы
Страниц, соответствующие несмежным областям памяти. Функции vmaiiocO
и vf гее () фактически ограничиваются обновлением главных Таблиц Страниц
ядра (то есть глобального каталога страниц initmm.pgd и Таблиц Страниц,
являющихся его потомками).
Однако после окончания инициализации ядра никакие процессы и потоки
ядра не пользуются главными Таблицами Страниц ядра напрямую. Рассмот-
Рассмотрим, что происходит при первом обращении процесса к несмежной области
памяти в режиме ядра. Когда линейный адрес транслируется в физический
адрес, блок управления памятью встречает нулевую запись Таблицы Страниц
и возбуждает исключение "ошибка обращения к странице". Обработчик рас-
распознает этот специальный случай, потому что исключение возникло в режи-
режиме ядра, а ошибочный линейный адрес больше tasksize. Тогда обработчик
dopagef auit () проверяет соответствующую запись главной Таблицы Стра-
Страниц ядра:
vmalloc_f ault:
asm("movl %%cr3 ,%0":"=r" (pgd_paddr));
pgd = pgd_index(address) + (pgd_t *) va(pgd_paddr);
pgd_k = init_mm.pgd + pgd_index(address);
if (!pgd_present(*pgd_k))
goto no_context;
pud = pud_offset(pgd, address);
pud_k = pud_offset(pgd_k, address);
if (!pud_present(*pud_k))
goto no_context;
pmd = pmd_offset(pud, address);
pind_k = pmd_offset(pud_k, address);
if (!pmd_present (*pmd_k) )
goto no_context;
s e t_pmd(pmd, * pmd_k);
pte_k = pte_offset_kernel(pmd_k, address);
if (!pte_present(*pte_k))
goto no_context;
return;
В локальную переменную pgdpaddr записывается физический адрес глобаль-
глобального каталога страниц текущего процесса, хранящийся в регистре сгЗ12. Затем
в локальную переменную pgd записывается линейный адрес, соответствую-
соответствующий адресу в переменной pgdpaddr, а в локальную переменную pgdk — ли-
линейный адрес главного глобального каталога страниц ядра.
Если запись главного глобального каталога страниц ядра, соответствующая
линейному адресу, вызвавшему ошибку, содержит одни нули, функция пере-
переходит на код за меткой nocontext (см. разд. "Обработка ошибочного адреса,
не входящего в адресное пространство" ранее в этой главе). В противном
случае функция читает запись главного верхнего каталога страниц ядра и
запись главного среднего каталога страниц ядра, которые соответствуют
12 Ядро не использует поле cur rent ->mm->pgd для получения адреса, потому что данная ошибка
может возникнуть в любой момент, даже во время переключения процесса.
ошибочному линейному адресу. И опять, если хотя бы одна из этих записей
содержит одни нули, функция совершает переход на метку nocontext. В про-
противном случае запись главного каталога копируется в соответствующую
запись среднего каталога страниц процесса13. После этого вся операция по-
повторяется с записью главной Таблицы Страниц.
Создание и удаление
адресного пространства процесса
В разд. "Адресное пространство процесса" ранее в этой главе мы перечис-
перечислили шесть типичных случаев, в которых процесс получает новые области
памяти. Первый случай, выполнение системного вызова fork (), требует соз-
создания целого адресного пространства для процесса-потомка. Когда же про-
процесс завершает работу, ядро уничтожает его адресное пространство. В этом
разделе мы обсудим, как происходит создание и удаление адресного про-
пространства процесса в операционной системе Linux.
Создание адресного пространства процесса
В главе 3 мы заметили, что ядро вызывает функцию соругат () при создании
нового процесса. Эта функция создает адресное пространство процесса, на-
настраивая все Таблицы Страниц и дескрипторы памяти нового процесса.
Как правило, у каждого процесса есть свое собственнное адресное простран-
пространство, но облегченные процессы могут быть созданы с помощью системного
вызова clone () при установленном флаге clonevm. Такие процессы совмест-
совместно используют одно адресное пространство, т. е. им разрешено обращаться к
одному и тому же набору страниц.
В соответствии с подходом "копирования при записи", описанным ранее,
традиционные процессы наследуют адресное пространство своего родителя:
страницы остаются совместно используемыми до тех пор, пока процессы
только читают их. Как только один из процессов попытается записать данные
на какую-либо страницу, создается ее копия. Как правило, через некоторое
время ответвленный процесс получает собственное адресное пространство,
отличное от адресного пространства родителя. Облегченный процесс, напро-
напротив, использует адресное пространство своего родителя. В Linux это реализо-
реализовано простым отказом от копирования адресного пространства. Облегченные
13 Читатель, конечно, помнит, что если механизм РАЕ включен, то запись верхнего каталога стра-
страниц не может быть нулевой. Если же он отключен, то внесение информации в запись среднего ката-
каталога страниц подразумевает внесение ее и в запись верхнего каталога страниц.
процессы создаются заметно быстрее обычных, и совместное использование
страниц можно считать большим достоинством при условии, что родитель и
потомки тщательно координируют свои действия.
Если новый процесс был создан с помощью системного вызова clone о, а
флаг clonevm в параметре flag был установлен, то функция copymmo пре-
предоставляет клону (процессу tsk) адресное пространство его родителя (про-
(процесса current):
if (clone_flags & CLONE_VM) {
atomic_inc (¤t->mm->mm_users) ;
spin_unlock_wait (¤t->ram->page_table_lock) ;
tsk->ram = current->mm;
tsk->active_mm = current->ram;
return 0;
}
Вызов функции spinuniockwait () гарантирует, что если спин-блокировка на
таблицы страниц процесса получена другим процессором, обработчик ис-
исключения "ошибка обращения к странице" не завершит свою работу, пока
она не будет освобождена. На практике, помимо защиты таблиц страниц, эта
спин-блокировка должна запрещать создание новых облегченных процессов,
СОВМеСТНО ИСПОЛЬЗуЮЩИХ ДеСКрИПТОр current->mm.
Если флаг clonevm не установлен, функция copymm () должна создать новое
адресное пространство (даже если память внутри этого пространства не вы-
выделяется, пока процесс не обратится по какому-то адресу). Функция выделяет
новый дескриптор памяти, сохраняет его адрес в поле mm дескриптора процес-
процесса tsk и копирует содержимое структуры current->mm в поле tsk->mm. Затем
она обновляет некоторые поля нового дескриптора:
tsk->mm = kmem_cache_alloc(mm_cachep, SLAB_KERNEL);
memcpy(tsk->mm, current->mm, sizeof(*tsk->mm) );
atomic_set(&tsk->ram->ram_users/ 1);
atoniic_set (&tsk->nim->ram_count, 1) ;
init_rwsem(&tsk->ram->ramap_sem) ;
tsk->ram->core_waiters = 0;
tsk->ram->page_table_lock = SPIN_LOCK_UNLOCKED;
tsk->ram->ioctx_list_lock = RW_LOCK_UNLOCKED;
tsk->mm->ioctx_list = NULL;
tsk->mm->default_kioctx = INIT_KIOCTX(tsk->ram->default_kioctx,
*tsk->mm);
tsk->mm->free_area_cache = (TASK_SIZE/3+0xfff)&0xfffff000;
tsk->ram->pgd = pgd_alloc (tsk->rnm) ;
tsk->ram->def_flags = 0;
Вспомним, что макрос pgdaiioco выделяет новому процессу глобальный
каталог страниц.
Затем вызывается архитектурно-зависимая функция initnewcontext (). Ра-
Работая на процессоре 80x86, эта функция проверяет, владеет ли текущий про-
процесс специализированной локальной таблицей дескрипторов. Если это так,
функция initnewcontext () создает копию этой таблицы и добавляет ее в
адресное пространство процесса tsk.
Наконец, вызывается функция dupmmapo, копирующая области памяти и
Таблицы Страниц процесса-родителя. Эта функция заносит новый дескрип-
дескриптор памяти tsk->nnn в глобальный список дескрипторов памяти. Затем она пе-
перебирает список областей, принадлежащих процессу-родителю, начиная с
области, на которую указывает поле current->ram->iranap. Функция копирует
каждый встреченный дескриптор области памяти vmareastruct и заносит
копию в список областей и в красно-черное дерево, принадлежащее процес-
процессу-потомку.
Непосредственно после этого функция dupmmapO вызывает функцию
copypagerange () для создания, если потребуется, Таблиц Страниц, необхо-
необходимых для отображения группы страниц, входящих в область памяти, и ини-
инициализации новых записей Таблицы Страниц. В частности, каждый странич-
страничный кадр, соответствующий закрытой и доступной для записи странице (флаг
vmshared сброшен, а флаг vmmaywrite установлен), помечается как доступ-
доступный только на чтение и родителю, и потомку. Это позволит в дальнейшем
использовать механизм копирования при записи.
Удаление адресного пространства процесса
Когда процесс завершает работу, ядро вызывает функцию exitram (), чтобы та
освободила адресное пространство, принадлежащее процессу:
ram_release(tsk, tsk->mm) ;
if (!(ram = tsk->ram)) /* kernel thread ? */
return;
down read(&rarn->ramap sera);
Функция ramreiease () возобновляет выполнение всех процессов, "спящих" в
completion-структуре tsk->vfork_done (см. главу 5). В типичном случае соот-
соответствующая очередь ожидания непуста, только если завершающий процесс
был создан с помощью системного вызова vf ork () (см. главу 3).
Если завершающийся процесс не является потоком ядра, функция exitmrao
должна освободить дескриптор памяти и все структуры, имеющие отношение
к процессу. Во-первых, она проверяет, установлен ли флаг ram->core_waiters.
Если установлен, значит, процесс выполняет дамп содержимого памяти в
файле core. Чтобы избежать порчи данных в файле core, функция использует
completion-структуры mm->core_done И mm->core_startup_done ДЛЯ сериализа-
ции выполнения облегченных процессов, совместно использующих дескрип-
дескриптор ПаМЯТИ ram.
Затем функция увеличивает главный счетчик обращений дескриптора памя-
памяти, сбрасывает поле mm дескриптора процесса и переводит процессор в "лени-
"ленивый" режим TLB (см. главу 2):
atomic_inc (&ram->ram_count) ;
spin_lock(tsk->alloc_lock);
tsk->ram = NULL;
up_read (&mm->map_sem) ;
enter_lazy_tlb(ram, current);
spin_unlock(tsk->alloc_lock);
mmput (mm) ;
Наконец, вызывается функция mmput (), которая освобождает локальную таб-
таблицу дескрипторов, дескрипторы областей памяти и Таблицы Страниц. Од-
Однако сам дескриптор памяти не освобождается, потому что функция
exitmmO увеличила главный счетчик обращений. Дескриптор будет освобо-
освобожден функцией finishtaskswitcho, когда локальный процессор фактиче-
фактически прекратит выполнение данного процесса (см. разд. "Функция schedule ()"
главы 7).
Управление кучей
У каждого процесса в Unix есть специфическая область памяти, называемая
кучей. Она служит для удовлетворения запросов процесса на динамическую
память. Поля startbrk и brk дескриптора памяти определяют, соответствен-
соответственно, начальный и конечный адреса этой области.
Для получения и освобождения динамической памяти процесс может вос-
воспользоваться следующими API-интерфейсами:
П maiioc(size) — запрашивает size байтов динамической памяти; в случае
успешного выделения памяти возвращает линейный адрес первой ячейки;
□ caiioc (n, size) — запрашивает массив из п элементов длины size; в случае
успешного выделения памяти инициализирует элементы массива нулями и
возвращает линейный адрес первого элемента;
□ reaiioc(ptr,size) — изменяет размер области памяти, ранее выделенной
ФУНКЦИЯМИ malloc () ИЛИ calloc ();
□ free(addr) — освобождает область памяти, выделенную функциями
maiioc () или caiioc () и начинающуюся с адреса addr;
□ brk(addr) — напрямую изменяет размер кучи; параметр addr задает новое
значение поля current->mm->brk? а возвращаемое значение является новым
конечным адресом области памяти (процесс должен проверить, совпадает
ли оно с запрошенным значением addr);
□ sbrk(incr) — аналогична функции brk(), но параметр incr определяет
увеличение или уменьшение кучи в байтах.
Функция brk() отличается от других перечисленных функций, потому что
только она реализована в виде системного вызова. Все остальные реализова-
реализованы в библиотеке С с использованием функций brk () и mmap (I4.
Когда процесс в режиме пользователя делает системный вызов brk (), ядро
вызывает функцию sysbrk(addr). Эта функция вначале проверяет, не попа-
попадает ли параметр addr в область памяти, содержащую код процесса. Если это
так, она немедленно возвращает управление, потому что куча не может пере-
пересекаться с областью, в которой находится код процесса:
ram = current->ram;
down_write (&ram->ramap sem) ;
if (addr < mm->end_code) {
out:
up_write (&mrn->mmap_sem) ;
return ram->brk;
}
Поскольку системный вызов brk () работает с областью памяти, он выделяет
и освобождает целые страницы. Поэтому функция выравнивает значение addr
так, чтобы оно было кратно числу pagesize, и сравнивает результат со зна-
значением поля brk дескриптора памяти:
newbrk = (addr + Oxfff) & OxfffffOOO;
oldbrk = (ram->brk + Oxfff) & OxfffffOOO;
if (oldbrk == newbrk) {
ram->brk = addr;
goto out;
}
Если процесс запросил уменьшение размера кучи, функция sysbrk () вызы-
вызывает функцию domunmap (), чтобы выполнить эту задачу, а затем возвращает
управление:
14 Библиотечная функция realloc () может также делать системный вызов mremap ().
if (addr <= ram->brk) {
if (!do_munmap(mm, newbrk, oldbrk-newbrk))
mm->brk = addr;
goto out;
}
Если же процесс попросил увеличить кучу, функция sysbrk () вначале про-
проверяет, разрешено ли это ему. Если процесс пытается выделить память, пре-
превышая свой лимит, функция просто возвращает прежнее значение поля
mm->brk, не предоставляя процессу дополнительную память:
rlim = current->signal->rlim[RLIMIT_DATA] .rlim_cur;
if (rlim < RLIM_INFINITY && addr - mm->start_data > rlim)
goto out;
Затем функция проверяет, не пересечется ли увеличенная куча с другой об-
областью памяти, принадлежащей процессу. Если пересечение возможно, функ-
функция возвращает управление, ничего не предпринимая:
if (find_vma_intersection(mm/ oldbrk, newbrk+PAGE_SIZE) )
goto out;
Если все в порядке, вызывается функция dobrk (). Если она возвращает зна-
значение oldbrk, значит, выделение памяти произошло успешно, и функция
sysbrk () возвращает значение addr; в противном случае она возвращает ста-
старое значение mm->brk:
if (do_brk(oldbrk, newbrk-oldbrk) == oldbrk)
mm->brk = addr;
goto out;
Функция dobrk () — это фактически упрощенная версия функции dommap (),
которая работает только с анонимными областями памяти. Ее вызов эквива-
эквивалентен вызову:
do_mmap(NULL, oldbrk, newbrk-oldbrk, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_FIXED|MAP_PRIVATE, 0)
Функция do_brk() работает немного быстрее, чем dommapo, потому что
опускает некоторые проверки полей объекта-области памяти исходя из пред-
предположения, что эта область памяти не отображает файл с диска.
ГЛАВА 10
Системные вызовы
Операционные системы предлагают процессам, работающим в режиме поль-
пользователя, набор интерфейсов для взаимодействия с аппаратными устройства-
устройствами, такими как процессор, диски или принтеры. Создание дополнительного
программного слоя между приложением и аппаратной частью компьютера
имеет ряд преимуществ. Во-первых, это облегчает программирование, пото-
потому что программисты избавлены от необходимости изучать низкоуровневые
характеристики аппаратных устройств. Во-вторых, такой подход существен-
существенно повышает безопасность системы, поскольку ядро может проверить кор-
корректность запроса на уровне интерфейса до выполнения этого запроса. Нако-
Наконец, что не менее важно, подобные интерфейсы делают программы более пе-
переносимыми, позволяя компилировать и корректно выполнять их в каждом
ядре, предлагающем такой же набор интерфейсов.
В Unix-системах большинство интерфейсов между процессами режима поль-
пользователя и аппаратными устройствами реализовано с помощью системных
вызовов, направляемых ядру. В этой главе подробно рассматривается, как
Linux реализует системные вызовы, передаваемые ядру пользовательскими
программами.
API-интерфейсы стандарта POSIX
и системные вызовы
Мы начнем с обсуждения различия между API (Application Programmer
Interface, интерфейс прикладного программирования) и системным вызовом.
Первый представляет собой определение функции, описывающее, как полу-
получить данную услугу, а второй является явным запросом к ядру, выполненным
с помощью программного прерывания.
Unix-системы включают в себя несколько библиотек функций, предостав-
предоставляющих программистам API-интерфейсы. Некоторые из интерфейсов, опре-
определенных в стандартной библиотеке С, называемой libc, обращаются к ин-
интерфейсным процедурам (процедурам, единственным назначением которых
является выдача системного вызова). Обычно каждый системный вызов име-
имеет интерфейсную процедуру, определяющую API-интерфейс, которому
должна следовать прикладная программа.
Между прочим, обратное неверно. Интерфейс API не обязательно соответст-
соответствует какому-либо системному вызову. Во-первых, API может предлагать свои
услуги непосредственно в режиме пользователя. (Для таких абстрактных
функций, какими являются математические, нет смысла делать системные
вызовы.) Во-вторых, одна API-функция может сделать несколько системных
вызовов. Более того, несколько API-функций могут делать один и тот же сис-
системный вызов, но "нагружать" его дополнительными особенностями. Напри-
Например, в Linux API-функции maiioc (), caiioc () и free () реализованы в библио-
библиотеке libc. Код в этой библиотеке отслеживает запросы на выделение и осво-
освобождение памяти и использует системный вызов brk() для увеличения или
уменьшения кучи процесса (см. главу 9).
Стандарт POSIX описывает API-интерфейсы, но не системные вызовы. Сис-
Система может быть сертифицирована как удовлетворяющая стандарту POSIX,
если она предоставляет прикладным программам надлежащий набор API-
интерфейсов, независимо от того, как реализованы соответствующие функ-
функции. На практике несколько систем, не являющихся Unix-подобными, были
сертифицированы как удовлетворяющие стандарту POSIX, поскольку они
предлагали все традиционные службы Unix в своих библиотеках режима
пользователя.
С точки зрения программиста, различие между API и системным вызовом не
имеет значения. Единственное, что ему нужно знать, — это имя функции,
типы параметров и смысл возвращаемого значения. Зато с точки зрения раз-
разработчика ядра это различие принципиально, ведь системные вызовы при-
принадлежат ядру, а библиотеки режима пользователя — нет.
Большинство интерфейсных процедур возвращает целое значение, смысл ко-
которого зависит от системного вызова. Код возврата —1, как правило, указыва-
указывает на то, что ядро не смогло удовлетворить запрос процесса. Неудача обра-
обработчика системного вызова могла быть следствием некорректных парамет-
параметров, недостатка системных ресурсов, аппаратных проблем и т. п. Конкретный
код ошибки содержится в переменной errno, которая определена в библиоте-
библиотеке libc.
Каждый код ошибки определен в виде макроконстанты, возвращающей по-
положительное значение. Стандарт POSIX устанавливает имена макросов для
некоторых кодов ошибок. В Linux, на платформе 80x86 , эти макросы опре-
определены в заголовочном файле include/asm-i386/errno.h. Для обеспечения пе-
переносимости программ на языке С между системами Unix заголовочный файл
include/asm-i386/errno.h, в свою очередь, включен в стандартный заголовоч-
заголовочный файл библиотеки С, называемый /usr/include/errno.h. Другие системы
имеют свои собственные специализированные подкаталоги для заголовочных
файлов.
Обработчик системного вызова
и служебные процедуры
Когда процесс режима пользователя делает системный вызов, процессор пе-
переключается в режим ядра и приступает к выполнению функции ядра. Как мы
увидим в следующем разделе, в архитектуре 80x86 системный вызов Linux
может быть сделан двумя разными способами. Впрочем, в обоих случаях ре-
результатом будет переход на некоторую ассемблерную функцию, называемую
обработчиком системного вызова.
Поскольку в ядре реализовано много разных системных вызовов, процесс
режима пользователя должен передавать параметр, содержащий номер сис-
системного вызова, который служит для идентификации вызова. В Linux для
этой цели используется регистр еах. Как мы увидим в разд. "Передача пара-
параметров" далее в этой главе, при совершении системного вызова обычно пе-
передаются и дополнительные параметры.
Все системные вызовы возвращают целые значения. Соглашения, принятые
для этих возвращаемых значений, отличны от тех, что приняты для интер-
интерфейсных процедур. В ядре положительное или нулевое значение свидетель-
свидетельствует об успешном завершении системного вызова, а отрицательное — об
ошибке. В последнем случае абсолютная величина значения является кодом
ошибки, который должен быть возвращен прикладной программе в перемен-
переменной errno. Ядро никак не использует эту переменную, зато интерфейсные
процедуры записывают в нее значение после возврата управления от систем-
системного вызова.
Обработчик системного вызова, имеющий структуру, аналогичную структуре
других обработчиков прерываний, выполняет следующие действия:
□ сохраняет содержимое большинства регистров в стеке режима ядра (эта
операция является общей для всех системных вызовов, и она закодирована
на языке ассемблера);
□ обрабатывает системный вызов с помощью специальной функции, напи-
написанной на языке С, которая называется служебной процедурой системного
вызова',
□ выполняет корректный выход: загружает в регистры значения, сохранен-
сохраненные в стеке режима ядра, и переключает процессор из режима ядра в ре-
режим пользователя (эта операция является общей для всех системных вы-
вызовов, и она закодирована на языке ассемблера).
Имя служебной процедуры, ассоциированной с системным вызовом xyz(),
обычно имеет формат sysxyz (); впрочем, из этого правила есть несколько
исключений.
На рис. 10.1 проиллюстрированы взаимоотношения между прикладной про-
программой, сделавшей системный вызов, соответствующей интерфейсной про-
процедурой, обработчиком системного вызова и служебной процедурой систем-
системного вызова. Стрелки обозначают передачу управления от одной функции к
другой. Конструкции "syscall" и "sysexit" являются условными заменителя-
заменителями реальных команд на языке ассемблера, которые переключают процессор,
соответственно, из режима пользователя в режим ядра и обратно.
Рис. 10.1. Выполнение системного вызова
Чтобы ассоциировать номер каждого системного вызова с соответствующей
служебной процедурой, ядро пользуется таблицей диспетчеризации систем-
системных вызовов, которая хранится в массиве syscaiitabie и содержит
NRsyscaiis записей B89 в ядре Linux 2.6.11). В таблице п-я запись содержит
адрес служебной процедуры для системного вызова с номером п.
Макрос NRsyscaiis является всего лишь статическим ограничением количе-
количества системных вызовов, реализуемых в системе; он не показывает фактиче-
фактическое количество реализованных системных вызовов. В реальности любая
запись таблицы может содержать адрес функции sysnisyscaii (), которая
представляет собой служебную процедуру "нереализованных" системных
вызовов и просто возвращает код ошибки -enosys.
Вход в системный вызов и выход из него
Приложения, "родные" для Linux1, могут делать системные вызовы двумя
различными способами:
□ выполнив ассемблерную инструкцию int $0x8о; в ранних версиях ядра
Linux это был единственный способ переключения из режима пользовате-
пользователя в режим ядра;
П выполнив ассемблерную инструкцию sysenter, появившуюся в микропро-
микропроцессорах Intel Pentium II; эта инструкция теперь поддерживается ядром
Linux 2.6.
Аналогичным образом, ядро может выйти из системного вызова (и тем са-
самым вернуть процессор в режим пользователя) двумя способами:
□ выполнив ассемблерную инструкцию iret;
□ выполнив ассемблерную инструкцию sysexit, появившуюся в микропро-
микропроцессорах Intel Pentium II, вместе с инструкцией sysenter.
Однако поддержка двух разных способов входа в ядро является не таким
простым делом, как может показаться:
П ядро должно поддерживать как старые библиотеки, в которых использует-
используется только инструкция int $0x80, так и новые, в которых используется и
ИНСТРУКЦИЯ sysenter;
□ стандартная библиотека, в которой применяется только инструкция
sysenter, должна уметь работать со старыми версиями ядра, которые под-
поддерживают только инструкцию int $0x80;
П1 ядро и стандартная библиотека должны уметь работать как на старых про-
процессорах, не имеющих инструкции sysenter, так и на новых, у которых
она есть.
Мы увидим, как Linux справляется с этими трудностями, в разд. "Выполнение
системного вызова с помощью инструкции sysenter" далее в этой главе.
Выполнение системного вызова
с помощью инструкции int $0x80
"Традиционным" способом выполнения системного вызова является выпол-
выполнение ассемблерной инструкции int, которая была представлена в главе 4.
1 Как мы увидим в разд. "Области выполнения" главы 20, Linux может выполнять программы, от-
откомпилированные под другие операционные системы. Следовательно, ядро обеспечивает режим
совместимости для входа в системный вызов. Процессы режима пользователя, выполняющие про-
программы операционных систем iBCS и Solaris /x86, могут перейти в режим ядра с помощью подхо-
подходящего шлюза вызовов из принятой по умолчанию локальной таблицы дескрипторов (см. разд. "The
Linux LDTs" в главе 2).
Вектор 128 @x80 в шестнадцатеричной системе счисления) ассоциирован с
точкой входа в ядро. Функция trapinit (), вызываемая на этапе инициализа-
инициализации ядра, устанавливает запись таблицы дескрипторов прерываний, соответ-
соответствующую вектору 128, следующим образом:
set_system_gate@x80, &system_call);
Этот код загружает в поля дескриптора шлюза (см. главу 4) следующие зна-
значения:
□ селектор сегмента — селектор сегмента кода ядра kernelcs;
□ смещение — указатель на обработчик системного вызова systemcaii ();
□ тип — число 15, означающее, что исключение имеет тип Trap, а соответст-
соответствующий обработчик не отключает маскируемые прерывания;
□ DPL (Descriptor Privilege Level, уровень привилегий дескриптора) — чис-
число 3, означающее, что процессам режима пользователя разрешено
вызывать обработчик исключений (см. главу 4).
Таким образом, когда процесс режима пользователя выдает инструкцию int
$0x80, процессор переключается в режим ядра и приступает к выполнению
инструкций, начиная с адреса systemcaii.
Функция system_call()
Функция systemcaii о начинает работу с того, что сохраняет в стеке номер
системного вызова и все регистры процессора, которые могут понадобиться
обработчику исключений, кроме регистров ef lags, cs, eip, ss и esp, автомати-
автоматически сохраненных блоком управления. Макрос saveall, описанный в гла-
главе 4, загружает селектор сегмента данных ядра в регистры ds и es:
system_call:
pushl %eax
SAVE_ALL
movl $0xffffe000, %ebx /* or OxfffffOOO for 4-KB stacks */
andl %esp, %ebx
Затем функция сохраняет адрес структуры threadinfo текущего процесса
в регистре ebx (см. главу 3). С этой целью значение указателя на стек ядра
округляется до числа, кратного 4 или 8 Кбайт.
После этого функция systemcaii () проверяет, установлен ли хотя бы один из
флаГОВ TIF_SYSCALL_TRACE И TIF_SYSCALL_AUDIT В ПОЛв flags Структуры
threadinfo, т. е. отслеживает ли отладчик системные вызовы, которые дела-
делает выполняемая программа. В случае положительного результата проверки
функция systemcaii о дважды вызывает функцию dosyscaiitraceo: непо-
средственно перед и сразу после выполнения служебной процедуры данного
системного вызова. Эта функция останавливает процесс current и позволяет
отлаживающему процессу собрать информацию о нем.
Затем проверяется допустимость номера системного вызова, переданного
процессом режима пользователя. Если он больше или равен количеству
записей в таблице передачи системных вызовов, обработчик системного вы-
вызова завершает работу:
cmpl $NR_syscalls, %еах
jb nobadsys
movl $(-ENOSYS), 24(%esp)
jmp resume_userspace
nobadsys:
Если номер системного вызова оказался недопустимым, функция записывает
значение -enosys в ячейку стека, в который был сохранен регистр еах, т. е. в
ячейку со смещением 24 от текущей верхушки стека. Затем функция перехо-
переходит по адресу resumeuserspace (см. далее). Таким образом, когда процесс
возобновит свое выполнение в режиме пользователя, он обнаружит в регист-
регистре еах отрицательный код возврата.
Наконец, функция вызывает служебную процедуру, ассоциированную с но-
номером системного вызова, хранящимся в регистре еах:
call *sys_call_table(O, %eax, 4)
Поскольку в таблице системных вызовов каждая запись имеет длину 4 байта,
ядро находит адрес нужной служебной процедуры, умножая номер системно-
системного вызова на 4, прибавляя к произведению начальный адрес таблицы
syscaiitabie и извлекая указатель на служебную процедуру из соответст-
соответствующей записи в таблице.
Выход из системного вызова
Когда служебная процедура системного вызова завершает работу, функция
systeincaii () получает код возврата из регистра еах и записывает его в то
место стека, где было сохранено содержимое регистра еах в режиме пользо-
пользователя:
movl %еах, 24(%esp)
В результате процесс режима пользователя найдет код возврата системного
вызова в регистре еах.
Затем функция systemcaiio отключает локальные прерывания и проверяет
флаги В структуре thread_infо процесса current:
cli
movl 8(%ebp), %ecx
testw $0xffff, %cx
je restore_all
Поле flags имеет смещение 8 в структуре threadinfo, а маска Oxf f f f выде-
выделяет биты, соответствующие всем флагам, перечисленным в табл. 4.15, кроме
флага tifpollingnrflag. Если ни один из этих флагов не установлен, функ-
функция переходит на метку restoreaii. Как сказано в главе 4, код, расположен-
расположенный по этому адресу, восстанавливает содержимое регистров, сохраненное в
стеке режима ядра, и выполняет ассемблерную инструкцию iret, чтобы во-
возобновить выполнение процесса в режиме пользователя (см. блок-схему на
рис. 4.6).
Если хотя бы один из флагов установлен, функция должна проделать опреде-
определенную работу до возвращения в режим пользователя. Если установлен флаг
TIFSYSCALLTRACE, фуНКЦИЯ systemcall () ВТОрОЙ раз ВЫЗЫВаеТ фуНКЦИЮ
do_syscaii_trace(), а затем переходит на метку resume_userspace. Если же
флаг TIFSYSCALLTRACE сброшен, фуНКЦИЯ ПереХОДИТ На Метку work_pending.
КОД, расположенный ПО адресам resume_userspace И workpending, проверяет
наличие запроса на перепланирование процессов, режим виртуального 8086,
наличие сигналов, ожидающих доставки, и режим пошагового выполнения.
Затем в любом случае совершается переход на метку restoreaii, чтобы во-
возобновилось выполнение процесса в режиме пользователя.
Выполнение системного вызова
с помощью инструкции sysenter
Ассемблерная инструкция int является медленной по своей природе, потому
что она выполняет ряд проверок на непротиворечивость и безопасность. (Эта
инструкция подробно описана в главе 4.)
Инструкция sysenter, получившая в документации Intel название "Быстрый
системный вызов", предоставляет более быстрый способ переключения из
режима пользователя в режим ядра.
Функция sysenter
Ассемблерная инструкция sysenter использует три специальных регистра,
в которые должна быть загружена следующая информация2:
2 "MSR" является сокращением для "Model-Specific Register" (регистр, специфичный для модели) и
обозначает регистр, имеющийся только в некоторых моделях микропроцессоров 80x86.
□ sysentercsmsr — селектор сегмента кода ядра;
□ sysenter_eip_msr — линейный адрес точки входа;
□ sysenter_esp_msr — указатель стека ядра.
Когда выполняется инструкция sysenter, блок управления процессора:
□ копирует содержимое регистра sysentercsmsr в регистр cs;
П копирует содержимое регистра sysentereipmsr в регистр eip;
□ копирует содержимое регистра sysenterespmsr в регистр esp;
□ прибавляет 8 к значению в регистре sysentercsmsr и загружает результат
в регистр ss.
Таким образом, процессор переключается в режим ядра и начинает выпол-
выполнять первую инструкцию в точке входа ядра. Как мы видели в главе 2, сег-
сегмент стека ядра совпадает с сегментом данных ядра, а соответствующий де-
дескриптор следует за дескриптором сегмента кода ядра в глобальной таблице
дескрипторов. Следовательно, на шаге 4 в регистр ss загружается правиль-
правильный селектор сегмента.
Три регистра, специфичных для модели, инициализируются функцией
enabiesepcpu (), которая выполняется один раз каждым процессором в сис-
системе на этапе инициализации ядра. Эта функция выполняет следующие дей-
действия:
1. Записывает селектор сегмента кода ядра ( kernelcs) в регистр
SYSENTER_CS_MSR.
2. Записывает линейный адрес функции sysenterentryO в регистр
SYSENTER_CS_EIP.
3. Вычисляет линейный адрес конца локального сегмента TSS и записывает
это значение в регистр sysentercsesp3.
Здесь необходимо прокомментировать запись значения в регистр sysenter_
csesp. Когда начинается выполнение системного вызова, стек ядра пуст, и,
следовательно, регистр esp должен указывать на конец четырех- или восьми-
килобайтовой области памяти, включающей в себя стек ядра и дескриптор
текущего процесса (см. рис. 3.2). В режиме пользователя интерфейсная про-
процедура не может корректно установить этот регистр, поскольку она не знает
адреса этой области памяти. С другой стороны, значение должно быть запи-
J Кодирование адреса локального сегмента TSS, записанного в регистр SYSENTER_ESP_MSR, про-
происходит вследствие того факта, что регистр должен указывать на реальный стек, который растет в
направлении уменьшения адресов. На практике сработает инициализация регистра любым значени-
значением, при условии, что по этому значению можно будет получить адрес локального сегмента TSS.
сано в регистр до переключения в режим ядра. Поэтому ядро инициализирует
регистр, чтобы закодировать адрес сегмента состояния задачи (TSS) локаль-
локального процессора. Как было сказано в описании шага 3 функции switchto ()
(см. главу 3), при каждом переключении процесса ядро сохраняет указатель
стека ядра, которым пользуется текущий процесс, в поле espO локального
сегмента TSS. Таким образом, обработчик системного вызова считывает со-
содержимое регистра esp, вычисляет адрес поля espO локального сегмента TSS
и загружает "правильный" указатель стека ядра в регистр esp.
Страница vsyscall
Интерфейсная функция из стандартной библиотеки libc может применять ин-
инструкцию sysenter, только если эту инструкцию поддерживает как процес-
процессор, так и ядро Linux.
Эта проблема совместимости требует довольно сложного решения. Фактиче-
Фактически, на этапе инициализации функция sysentersetup () строит страничный
кадр, называемый страницей vsyscall^ который содержит небольшой совме-
совместно используемый ELF-объект (то есть маленькую динамическую ELF-
библиотеку). Когда процесс делает системный вызов execve (), чтобы выпол-
выполнить ELF-программу, код на странице vsyscall динамически компонуется с
адресным пространством процесса (см. разд. "Функции exec" главы 20). Код
на странице vsyscall использует наиболее подходящую инструкцию для вы-
выдачи системного вызова.
Функция sysentersetup () выделяет новый страничный кадр для страницы
vsyscall и связывает его физический адрес с фиксированно отображаемым
линейным адресом fixvsyscall (cm. главу 2). Затем функция копирует на
страницу один из двух заранее определенных совместно используемых ELF-
объектов:
□ если процессор не поддерживает инструкцию sysenter, функция строит
страницу vsyscall, включающую в себя такой код:
kernel_vsyscall:
int
$0x80
ret
□ если же процессор поддерживает инструкцию sysenter, функция строит
страницу vsyscall, включающую в себя такой код:
kernel_vsyscall:
pushl %ecx
pushl %edx
pushl %ebp
movl %esp, %ebp
sysenter
Когда интерфейсная процедура из стандартной библиотеки должна сделать
системный вызов, она вызывает функцию kerneivsyscaii (), независимо от
того, каков код последней.
Еще одна проблема совместимости возникает из-за того, что старые версии
ядра Linux не поддерживают инструкцию sysenter. В этом случае, конечно,
ядро не строит страницу vsyscall, а функция kerneivsyscaii о не компону-
компонуется с адресным пространством процессов в режиме пользователя. Когда
функции из новых стандартных библиотек распознают этот факт, они просто
выполняют инструкцию int $0x80 для выполнения системного вызова.
Вход в системный вызов
Последовательность действий, выполняемых, когда системный вызов делает-
делается с помощью инструкции sysenter, такова:
1. Интерфейсная процедура из стандартной библиотеки загружает номер
СИСТеМНОГО ВЫЗОВа В регистр еах И ВЫЗЫВаеТ фуНКЦИЮ kernel_
vsyscall ().
2. Функция kerneivsyscaiio сохраняет в стеке режима пользователя со-
содержимое регистров ebp, edx и есх (эти регистры будут использованы об-
обработчиком системного вызова), копирует указатель пользовательского
стека в регистр ebp и выполняет инструкцию sysenter.
3. Процессор переключается из режима пользователя в режим ядра, и ядро
приступает к выполнению функции sysenterentryo (на которую указы-
указывает регистр sysenter_eip_msr).
4. Функция sysenterentryo, написанная на ассемблере, выполняет сле-
следующие действия:
• устанавливает указатель стека ядра:
movl -508(%esp), %esp
• изначально регистр esp указывает на адрес, непосредственно следую-
следующий за локальным сегментом TSS, имеющим длину 512 байтов. Следо-
Следовательно, инструкция загружает в регистр esp содержимое поля,
имеющего смещение 4 в локальном сегменте TSS, т. е. содержимое по-
поля espO. Как было сказано ранее, в поле espO всегда находится указатель
стека ядра для текущего процесса;
• включает локальные прерывания:
sti
• сохраняет в стеке режима ядра селектор сегмента пользовательских
данных, текущий указатель пользовательского стека, регистр efiags,
селектор сегмента пользовательского кода и адрес инструкции, кото-
которую следует выполнить при выходе из системного вызова:
pushl $( USER_DS)
pushl %ebp
pushfl
pushl $( USER_CS)
pushl $SYSENTER_RETURN
• заметьте, что эти инструкции эмулируют некоторые операции, выпол-
выполняемые ассемблерной инструкцией int (см. главу 4);
• восстанавливает в регистре ebp его оригинальное значение, переданное
интерфейсной процедурой:
movl (%ebp), %ebp
• эта инструкция делает именно то, что нужно, поскольку функция
kerneivsyscaiio сохранила в стеке режима пользователя ориги-
оригинальное значение регистра ebp, а затем загрузила в этот регистр теку-
текущее значение указателя пользовательского стека;
• обращается к обработчику системного вызова, выполняя последова-
последовательность инструкций, идентичную той, что начинается за меткой
systemcaii (ранее в этой главе).
Выход из системного вызова
Когда служебная процедура системного вызова завершает работу, функция
sysenterentry() выполняет практически те же действия, что и функция
systemcaii () (см. предыдущий раздел). Во-первых, она считывает код воз-
возврата служебной процедуры, хранящийся в регистре еах, и сохраняет его
в той ячейке стека ядра, в которой было сохранено значение регистра еах
в режиме пользователя. Затем функция отключает локальные прерывания и
проверяет флаги в структуре threadinf о процесса current.
Если хотя бы один из флагов установлен, значит, перед возвратом в режим
пользователя должна быть проделана определенная работа. Чтобы избежать
дублирования кода, этот случай обрабатывается точно так же, как и в функ-
функции systemcaii (), Т.е. управление передается ПО адресу resume_userspace
ИЛИ work_pending (СМ. рис. 4.6).
В конечном счете ассемблерная инструкция iret извлекает из стека режима
ядра пять аргументов, сохраненных функцией sysenterentry () на шаге 4,
что приводит к переключению процессора в режим пользователя и выполне-
выполнению кода, расположенного за меткой sysenterreturn.
Если функция sysenterentryO обнаруживает, что все флаги сброшены, она
выполняет быстрый возврат в режим пользователя:
movl 40(%esp), %edx
movl 52(%esp), %ecx
xorl %ebp, %ebp
sti
sysexit
В регистры edx и есх загружаются значения из стека, сохраненные функцией
sysenterentry () на шаге 4 (см. предыдущий раздел). Регистр edx получает
адрес метки sysenterreturn, а регистр есх — текущий указатель пользова-
пользовательского стека.
Инструкция sysexit
Ассемблерная инструкция sysexit является парной для инструкции sysenter:
она позволяет быстро переключиться из режима ядра в режим пользователя.
Когда эта инструкция выполняется, управляющий блок процессора соверша-
совершает следующие действия:
1. Прибавляет 16 к значению в регистре sysentercsmsr и загружает резуль-
результат в регистр cs.
2. Копирует содержимое регистра edx в регистр eip.
3. Прибавляет 24 к значению в регистре sysentercsmsr и загружает резуль-
результат в регистр ss.
4. Копирует содержимое регистра есх в регистр esp.
Поскольку в регистре sysentercsmsr находится селектор сегмента кода яд-
ядра, в регистр cs загружается селектор сегмента пользовательского кода, а в
регистр ss — селектор сегмента пользовательских данных (см. главу 2).
В результате процессор переключается из режима ядра в режим пользовате-
пользователя и приступает к выполнению инструкции, адрес которой находится в реги-
регистре edx.
Код SYSENTERRETURN
Код за меткой sysenterreturn расположен на странице vsyscall, и он выпол-
выполняется, когда выполнение системного вызова, сделанного с помощью инст-
инструкции sysenter, Заканчивается С ПОМОЩЬЮ ИНСТРУКЦИИ iret ИЛИ sysexit.
Код просто восстанавливает оригинальное содержимое регистров ebp, edx и
есх, сохраненных в стеке режима пользователя, и возвращает управление
интерфейсной процедуре из стандартной библиотеки:
SYSENTER_RETURN:
popl %ebp
popl %edx
popl %ecx
ret
Передача параметров
Подобно обычным функциям, системные вызовы часто имеют входные и/или
выходные параметры, которые могут содержать числовые значения, адреса
переменных в адресном пространстве процесса режима пользователя или
даже адреса структур, содержащих указатели на функции режима пользова-
пользователя (см. разд. "Системные вызовы, связанные с обработкой сигналов"
в главе 11).
ПОСКОЛЬКУ фуНКЦИИ systemcall () И sysenterentry () ЯВЛЯЮТСЯ общИМИ ТОЧ-
ками входа для всех системных вызовов в Linux, у каждой есть хотя бы один
параметр — номер системного вызова, передаваемый через регистр еах. На-
Например, если прикладная программа вызывает интерфейсную процедуру
fork (), в регистр еах записывается число 2 (то есть NR_fork) до того, как
будет выполнена ассемблерная инструкция int $0x8о или sysenter. Посколь-
Поскольку значения в этот регистр записываются интерфейсными процедурами,
включенными в библиотеку libc, программистам обычно нет дела до номера
системного вызова.
Системному вызову fork () другие параметры не требуются. Однако многим
системным вызовам нужны дополнительные параметры, которые прикладная
программа должна передать явным образом. Например, системному вызову
mmap () может понадобиться до шести параметров (не считая номера систем-
системного вызова).
Параметры обычных функций на языке С, как правило, передаются путем
записи их значений в активный стек программы (либо стек режима пользо-
пользователя, либо стек ядра). Но поскольку системные вызовы являются особыми
функциями, пересекающими границу между территорией пользователя и тер-
территорией ядра, стеки режима пользователя и режима ядра не могут быть за-
задействованы. Параметры системных вызовов записываются в регистры про-
процессора перед выполнением системного вызова. Затем ядро копирует пара-
параметры из регистров процессора в стек режима ядра до обращения к
служебной процедуре системного вызова, т. к. последняя является обычной
функцией на языке С.
Почему ядро не копирует параметры непосредственно из стека режима поль-
пользователя в стек режима ядра? Во-первых, работать одновременно с двумя
стеками довольно трудно, а во-вторых, применение регистров делает струк-
структуру обработчика системного вызова аналогичной структуре обработчиков
исключений.
Однако, чтобы передать параметры через регистры, необходимо выполнение
двух условий:
□ длина каждого параметра не может превышать длину регистра C2 битаL;
□ количество параметров не может быть больше шести (не считая номер
системного вызова, передаваемый через регистр еах), поскольку количест-
количество регистров в архитектуре 80x86 весьма ограничено.
Первое условие выполнено всегда, поскольку, согласно стандарту POSIX,
большие параметры, не умещающиеся в 32-битовом регистре, должны пере-
передаваться по ссылке. Типичным примером является системный вызов
settimeofday (), который обращается к 64-битовой структуре.
Зато существуют системные вызовы, принимающие более шести параметров.
В таких случаях один регистр служит в качестве указателя на область памяти
в адресном пространстве процесса, которая содержит значения параметров.
Конечно, программисты не должны думать обо всех этих обходных манев-
маневрах. Как и в случае с обычной функцией на языке С, параметры автоматиче-
автоматически сохраняются в стеке, когда вызывается интерфейсная процедура. Эта
процедура найдет подходящий способ передачи параметров ядру.
Для хранения номера и параметров системного вызова используются сле-
следующие регистры, в порядке возрастания: еах (для номера системного вызо-
вызова), ebx, ecx, edx, esi, edi И ebp. Как МЫ видели ранее, функции systemcall ()
и sysenterentryo сохраняют значения этих регистров в стеке режима ядра
при помощи макроса saveall. Таким образом, когда служебная процедура
системного вызова обратится к стеку, она найдет адрес возврата в функцию
systemcall () или sysenterentry (), а за ним — параметр, сохраненный в ре-
регистре ebx (первый параметр системного вызова), параметр, сохраненный в
регистре есх, и т. д. (см. главу 4). Такая конфигурация стека в точности соот-
соответствует конфигурации для вызова обычной функции, и, следовательно,
служебная процедура легко найдет свои параметры с помощью обычных кон-
конструкций языка С.
Рассмотрим пример. Служебная процедура syswriteo, работающая с сис-
системным вызовом write (), объявлена следующим образом:
int sys_write (unsigned int fd, const char * buf, unsigned int count)
4 Как всегда, мы имеем в виду 32-разрядную архитектуру процессоров 80x86. Сказанное в этом
разделе не относится к 64-разрядным архитектурам.
Компилятор С генерирует функцию на языке ассемблера, которая ожидает
найти параметры fd, buf и count наверху стека, сразу под адресом возврата,
в ячейках, использованных для сохранения содержимого регистров ebx, есх и
edx соответственно.
В некоторых случаях, даже если системному вызову не требуются парамет-
параметры, соответствующая служебная процедура должна знать содержимое реги-
регистров процессора непосредственно перед выполнением системного вызова.
Например, функция do_fork (), реализующая системный вызов fork (), должна
знать содержимое регистров, чтобы продублировать их в поле thread процес-
процесса-потомка (см. главу 3). В таких случаях один параметр типа ptregs позво-
позволяет служебной процедуре найти значения, сохраненные в стеке режима ядра,
с помощью макроса saveall (см. главу 4):
int sys_fork (struct pt_regs regs)
Значение, возвращаемое служебной процедурой, должно быть записано в ре-
регистр еах. Это делается автоматически компилятором С, когда выполняется
оператор return n;.
Проверка параметров
Все параметры системного вызова должны быть тщательно проверены, пре-
прежде чем ядро попытается удовлетворить запрос. Вид проверки зависит как от
системного вызова, так и от конкретного параметра. Вернемся к системному
вызову write (), о котором мы говорили в предыдущем разделе. Параметр fd
должен представлять собой дескриптор файла и идентифицировать конкрет-
конкретный файл, поэтому функция syswriteO проверяет, действительно ли fd яв-
является дескриптором уже открытого файла, и разрешено ли процессу записы-
записывать в него данные (см. главу 1). Если хотя бы одно из этих условий не вы-
выполнено, функция должна возвратить отрицательное значение, в данном
случае — код ошибки -ebadf.
Впрочем, одна проверка является общей для всех системных вызовов. Если в
параметре задается адрес, ядро должно проверить, входит ли он в адресное
пространство процесса. Существуют два способа проведения этой проверки:
□ убедиться, что линейный адрес принадлежит адресному пространству
процесса, и, если это действительно так, что у содержащей его области
памяти имеются соответствующие права доступа;
□ убедиться, что линейный адрес меньше значения pageoffset (to есть что
он не попадает в диапазон адресов, зарезервированных для ядра).
В ранних версиях ядра Linux выполнялась проверка первого типа. Однако
она требует много времени, поскольку проводится для каждого параметра с
адресом, переданного системному вызову. Кроме того, в ней обычно нет
смысла, поскольку программы с такими ошибками встречаются не очень
часто.
Поэтому, начиная с версии 2.2, Linux выполняет проверку второго типа. Она
намного эффективнее, потому что не требует перебора дескрипторов облас-
областей памяти процесса. Очевидно, что это очень грубая проверка: требование,
чтобы линейный адрес был меньше, чем pageoffset, является необходимым,
но не достаточным условием его допустимости. Однако, ограничиваясь этой
грубой проверкой, ядро ничем не рискует, поскольку другие ошибки будут
выловлены позже.
Короче говоря, подобный подход состоит в том, чтобы отложить реальные
проверки до последнего момента, т. е. до того, как блок управления страни-
страницами оттранслирует линейный адрес в физический. В разд. "Динамическая
проверка адресов: код обработки исключения" далее в этой главе мы обсу-
обсудим, как обработчик исключения "ошибка обращения к странице" распознает
ошибочные адреса в режиме ядра, которые были переданы процессами ре-
режима пользователя в качестве параметров системного вызова.
У читателя может возникнуть вопрос: а зачем вообще проводится эта грубая
проверка. Такой тип проверки, на самом деле, исключительно важен для за-
защиты адресных пространств ядра от несанкционированного доступа. В гла-
главе 2 мы видели, что оперативная память отображается, начиная с pageoffset.
Это означает, что процедуры ядра могут обращаться ко всем страницам, при-
присутствующим в памяти. Следовательно, если бы грубая проверка не произво-
производилась, процесс режима пользователя мог бы передать в качестве параметра
адрес, принадлежащий адресному пространству ядра. Это позволило бы про-
процессу читать или записывать в любую страницу, находящуюся в памяти, не
вызывая исключения "ошибка обращения к странице".
Проверка адресов, передаваемых системным вызовам, выполняется макросом
accessoko, который принимает два параметра: addr и size. Макрос проверя-
проверяет интервал адресов между addr и addr + size - 1. Он фактически эквива-
эквивалентен следующей функции:
int access_ok(const void * addr, unsigned long size)
{
unsigned long a = (unsigned long) addr;
if (a + size < a ||
a + size > current_thread_info()->addr_limit.seg)
return 0;
return 1;
}
Эта функция вначале проверяет, превышает ли сумма addr + size (то есть
максимальный адрес) значение 232-1. Поскольку длинные целые без знака и
указатели представлены в компиляторе GNU С (дсс) в виде 32-битовых чи-
чисел, такая проверка равносильна проверке на переполнение. Кроме того,
функция проверяет, превышает ли сумма addr + size значение, хранящееся в
поле addriimit.seg структуры threadinfo процесса current. Обычно это
поле содержит значение pageoffset для нормальных процессов и Oxf f f f f f f f
для потоков ядра. Значение в поле addriimit.seg может быть динамически
изменено макросами getf s и setf s, что позволяет ядру обходить проверки,
выполняемые макросом accessoko в целях безопасности, и обращаться к
служебным процедурам системных вызовов, напрямую передавая им адреса в
сегменте данных ядра.
Функция verif уагеа () делает ту же Проверку, ЧТО И макрос access_ok (). ХО-
ХОТЯ она и считается морально устаревшей, она по-прежнему часто встречается
в исходном коде.
Доступ к адресному пространству процесса
Служебным процедурам системных вызовов часто бывает необходимо читать
или записывать данные, хранящиеся в адресном пространстве процесса.
Linux предоставляет набор макросов, облегчающих эту задачу. Мы опишем
два из них: getuser () и putuser (). Первым можно воспользоваться для чте-
чтения одного, двух или четырех байтов по указанному адресу, а вторым — для
записи данных, имеющих эти размеры.
Каждая из этих функций принимает два аргумента, передаваемое значение х
и переменную ptr. Второй параметр также определяет количество пересы-
пересылаемых байтов. Так, в вызове get_user (x,ptr) размер переменной, на кото-
которую указывает ptr, заставляет функцию расшириться в одну из ассемблерных
функций get_user_i (), get_user_2 () или get_user_4 (). Рассмотрим, на-
например, функцию get_user_2 ():
get_user_2:
addl $1, %еах
jc bad_get_user
movl $0xffffe000, %edx /* or OxfffffOOO for 4-KB stacks */
andl %esp, %edx
cmpl 24(%edx), %eax
jae bad_get_user
2: movzwl
-l(%eax), %edx
xorl %eax, %eax
ret
bad_get_user:
xorl %edx, %edx
movl $-EFAULT, %eax
ret
Регистр еах содержит адрес ptr первого байта, который следует прочитать.
Первые шесть инструкций фактически выполняют те же проверки, что и мак-
макрос accessoko: они позволяют убедиться, что у двух считываемых байтов
адреса меньше, чем 4 Гбайт, и меньше значения в поле addriimit.seg про-
процесса current. (Это поле имеет смещение 24 в структуре threadinf о процесса
current, которое фигурирует в первом операнде инструкции cmpi.)
Если адреса являются допустимыми, функция выполняет инструкцию movzwi,
чтобы сохранить считываемые данные в двух младших байтах регистра edx и
обнулить старшие байты этого регистра. Затем она записывает в регистр еах
код возврата 0 и завершает работу. Если же адреса недопустимы, функция
очищает регистр edx, записывает в регистр еах значение -efault и заканчива-
заканчивает работу.
Макрос putuser (x,ptr) аналогичен макросу, описанному ранее, с той разни-
разницей, что он записывает значение х в адресное пространство процесса, начиная
с адреса ptr. В зависимости от размера х он вызывает либо макрос
put_user_asm() (размер 1, 2 или 4 байта), либо макрос put_user_u64 о
(размер 8 байтов). Оба макроса возвращают 0 в регистре еах, если запись
прошла успешно, или -efault в противном случае.
Для доступа к адресному пространству процесса из режима ядра существует
несколько других функций; они перечислены в табл. 10.1. Обратите внима-
внимание, что многие из них имеют версию с двумя символами подчеркивания ( )
в начале имени. Версии без такого префикса работают дольше, поскольку
проверяют допустимость запрошенного интервала линейных адресов, а вер-
версии с префиксом обходят эту проверку. Когда ядро должно многократно об-
обращаться к одной и той же области памяти в адресном пространстве процес-
процесса, гораздо эффективнее выполнить одну проверку адреса в самом начале, а
затем обращаться к области без дальнейших проверок.
Таблица 10.1. Функции и макросы, обращающиеся
к адресному пространству процесса
Функция Действие
get_user get_user Считывает целое значение из пространст-
пространства пользователя A, 2 или 4 байта)
put_user put_user Записывает целое значение в пространст-
пространство пользователя A,2 или 4 байта)
copy_f rom_user copy_f rom_user Копирует блок произвольного размера
из пространства пользователя
Таблица 10.1 (окончание)
Функция Действие
copy_to_user copy_to_user Копирует блок произвольного размера
в пространство пользователя
strncpy_f rom_user strncpy_f rom_user Копирует строку с завершающим нулевым
символом из пространства пользователя
strlen_user strnlen_user Возвращает длину строки с завершающим
нулевым символом, хранящейся
в пространстве пользователя
clear_user clear_user Заполняет нулями область памяти
в пространстве пользователя
Динамическая проверка адресов:
код обработки исключения
Как мы видели ранее, макрос accessoko выполняет грубую проверку допус-
допустимости линейных адресов, переданных в качестве параметров системного
вызова. Эта проверка лишь гарантирует, что процесс режима пользователя не
пытается манипулировать адресным пространством ядра. Тем не менее ли-
линейные адреса, переданные в качестве параметров, могут и не принадлежать
адресному пространству процесса. В этом случае, если ядро попытается об-
обратиться по одному из таких недопустимых адресов, возникнет исключение
"ошибка обращения к странице".
Прежде чем описать, как ядро распознает такой тип ошибки, мы выделим си-
ситуации, в которых исключение "ошибка обращения к странице" может воз-
возникнуть в режиме ядра. Обработчик этого исключения должен отличать одну
ситуацию от другой, потому что они требуют разных действий:
□ Ядро пытается обратиться к странице, принадлежащей адресному про-
пространству процесса, но либо соответствующий страничный кадр отсутст-
отсутствует, либо ядро пытается записать данные на страницу, доступную только
для чтения. В таких случаях обработчик должен выделить и инициализи-
инициализировать новый страничный кадр (см. главу 9).
□ Ядро обращается к странице в своем адресном пространстве, но соответ-
соответствующая запись в Таблице Страниц еще не проинициализирована (см.
главу 9). В этом случае ядро должно правильно заполнить некоторые запи-
записи в Таблицах Страниц текущего процесса.
□ Некоторые функции ядра содержат программные ошибки, в результате
чего при выполнении программы возникает исключение. Возможно также,
что исключение возникает вследствие непостоянного аппаратного сбоя.
Когда это происходит, обработчик должен выполнить дамп, называемый
"kernel oops" (см. главу 9).
□ Случай, представленный в этой главе: служебная процедура системного
вызова пытается читать или записывать в область памяти, адрес которой
передан в качестве параметра системного вызова, но этот адрес не при-
принадлежит адресному пространству процесса.
Обработчик исключения "ошибка обращения к странице" легко распознает
первый случай, определив, что адрес, вызвавший ошибку, принадлежит од-
одной из областей памяти процесса.
Обработчик может распознать и второй случай, проверив, содержит ли соот-
соответствующая строка Таблицы Страниц ядра непустую запись, отображаю-
отображающую адрес. Теперь мы опишем, как обработчик различает остальные случаи.
Таблицы исключений
Ключом к определению источника ошибки обращения к странице является
ограниченность круга вызовов, которые ядро может сделать, для обращения к
адресному пространству процесса. Лишь небольшая группа функций и мак-
макросов, описанных в предыдущем разделе, используется для этих целей.
Следовательно, если исключение вызвано недопустимым параметром, то ин-
инструкция, приведшая к исключению, обязательно содержится в одной из
функций или была сгенерирована макросом. Количество инструкций, обра-
обращающихся к адресному пространству пользователя, весьма невелико.
Поэтому не составляет труда поместить адрес каждой инструкции ядра, об-
обращающейся к адресному пространству процесса, в структуру, называемую
таблицей исключений. Если мы сделаем это, все остальное просто. Когда в
режиме ядра возникнет исключение "ошибка обращения к странице", обра-
обработчик dopagef auit () просмотрит таблицу исключений. Если она содержит
инструкцию, вызвавшую исключение, значит, причина ошибки в недопусти-
недопустимом параметре системного вызова. В противном случае причины более серь-
серьезные.
В Linux определено несколько таблиц исключений. Главная таблица исклю-
исключений генерируется компилятором С при сборке образа ядра. Она хранится в
секции extabie сегмента кода ядра, и ее начальный и конечный адреса
идентифицируются символами, тоже сгенерированными компилятором С:
start ex_table И stop ex_table.
Кроме того, каждый динамически загружаемый модуль ядра (см. приложе-
приложение 2) содержит свою локальную таблицу исключений. Эта таблица автома-
тически сгенерируется компилятором С при сборке образа модуля, и она за-
загружается в память, когда модуль подключается к работающему ядру.
Каждая запись таблицы исключений представляет собой структуру
exceptiontableentry, СОСТОЯЩУЮ ИЗ двух ПОЛеЙ:
□ insn— линейный адрес инструкции, которая обращается к адресному
пространству процесса;
□ fixup— адрес кода на языке ассемблера, который выполняется, когда
ошибка обращения к странице возникает "по вине" инструкции, располо-
расположенной по адресу insn.
Код обработки исключения состоит из нескольких ассемблерных инструкций,
решающих проблему, вызвавшую исключение. Как мы увидим далее в этом
разделе, обработка обычно заключается во вставке последовательности инст-
инструкций, заставляющих служебную процедуру возвратить процессу режима
пользователя код ошибки. Эти инструкции, как правило, определенные в том
же макросе (функции), который обращается к адресному пространству про-
процесса, помещаются компилятором С в отдельную секцию сегмента кода ядра,
называемую .fixup.
Функция searchexceptiontabies о применяется для поиска указанного ад-
адреса во всех таблицах исключений. Если адрес находится в какой-либо таб-
таблице, функция возвращает указатель на соответствующую структуру
exceptiontableentry; в противном случае она возвращает null. Таким обра-
образом, обработчик dopagef auit () выполняет следующие операторы:
if ((fixup = search_exception_tables(regs->eip))) {
regs->eip = fixup->fixup;
return 1;
}
Поле regs->eip содержит значение регистра eip, сохраненное в стеке режима
ядра, когда возникло исключение. Если значение регистра (указатель на ин-
инструкцию) находится в таблице исключений, обработчик dopagef auit () за-
заменяет сохраненное значение на адрес из записи, которую возвратила функ-
функция searchexceptiontabies (). Затем обработчик исключения "ошибка об-
обращения к странице" завершает свою работу, а прерванная программа
возобновляет свою с того, что выполняет код обработки.
Генерирование таблицы исключений
и кода обработки
Директива .section ассемблера GNU Assembler позволяет программистам
указать, в какой секции исполняемого файла содержится код, идущий следом
за директивой. Как мы увидим в главе 20, исполняемый файл включает в себя
сегмент кода, который, в свою очередь, может быть разбит на секции. Приве-
Приведем ассемблерные инструкции, которые добавляют запись в таблицу исклю-
исключений (атрибут "а" показывает, что секция должна быть загружен в память
вместе с остальной частью образа ядра:
.section ex_table, "a"
. long
адре с_плохой_инс трукции
адрес_кода_обработки
.previous
Директива .previous заставляет ассемблер вставить последующий код в сек-
секцию, которая была активной, когда была встречена последняя директива
.section.
Вернемся К функциям get_user_l (), get_user_2() И get_user_4 (), рас-
смотренным ранее. Инструкции, которые обращаются к адресному простран-
пространству процесса, имеют метки 1, 2 и з:
get_user_l :
[...]
1: movzbl (%eax), %edx
[...]
get_user_2:
С.]
2: movzwl -1(%еах), %edx
[...]
get_user_4:
3: movl -3(%eax), %edx
[...]
bad_get_user:
xorl %edx, %edx
movl $-EFAULT, %eax
ret
.section ex_table,"a"
.long lb, bad_get_user
.long 2b, bad_get_user
.long 3b, bad_get_user
.previous
Каждая запись таблицы исключений состоит из двух меток. Первая представ-
представляет собой число с суффиксом ь, показывающим, что метка находится мсза-
ди" (от англ. "backward11 — "назад"), т. е. расположена в одной из предыду-
предыдущих строчек программы. Код обработки является общим для всех трех функ-
функций и помечен как badgetuser. Если ошибка обращения к странице будет
вызвана инструкцией с меткой 1, 2 или з, выполнится код обработки. Он про-
просто возвращает -еfault процессу, сделавшему системный вызов.
Другие функции ядра, действующие в адресном пространстве режима поль-
пользователя, тоже применяют технику кода обработки. Рассмотрим в качестве
примера макрос strienuser (string). Он возвращает либо длину строки с за-
завершающим нулем, переданной в качестве параметра системному вызову,
либо 0 в случае ошибки. Макрос генерирует следующие ассемблерные инст-
инструкции:
movl $0, %еах
movl $0x7fffffff, %ecx
movl %ecx, %ebx
movl string, %edi
0: repne; scasb
subl %ecx, %ebx
movl %ebx, %eax
1:
.section .fixup,"ax"
2: xorl %eax, %eax
jmp lb
.previous
.section ex table,"a"
.long Ob, 2b
.previous
Регистры есх и ebx инициализируются значением 0x7fffffff, представляю-
представляющим максимальную разрешенную длину строки в адресном пространстве
режима пользователя. Ассемблерные инструкции repne;scasb итеративно
сканируют строку, на которую указывает регистр edi, в поисках нулевого
значения (символа \о, обозначающего конец строки). Поскольку инструкция
scasb уменьшает регистр есх на каждой итерации, регистр еах, в конечном
счете, будет содержать количество байтов в просканированной строке (то
есть ее длину).
Код обработки исключения, содержащийся в этом макросе, вставлен в сек-
секцию . f ixup. Атрибуты "ах" показывают, что секция должна быть загружена в
память и содержит выполняемый код. Если ошибка обращения к странице
будет вызвана инструкцией с меткой о, выполнится код обработки исключе-
исключения. Он просто загружает 0 в регистр еах, заставляя макрос возвратить код
ошибки 0 вместо длины строки, а затем переходит на метку 1, т. е. на инст-
инструкцию, следующую за макросом.
Вторая директива .section добавляет запись, содержащую адрес пары инст-
инструкций repne; scasb и адрес соответствующего кода обработки в секцию
ex__table.
Интерфейсные процедуры ядра
Хотя системные вызовы применяются, в основном, процессами режима
пользователя, они могут быть использованы и потоками ядра, которые не в
состоянии вызывать библиотечные функции. Для упрощения объявлений со-
соответствующих интерфейсных процедур в операционной системе Linux оп-
определен набор из семи макросов, носящих имена от syscaiio до _syscaii6.
В имени макроса цифра от 0 до 6 соответствует количеству параметров, ис-
используемых в системном вызове (не считая номера системного вызова). Эти
макросы применяются для объявления интерфейсных процедур, еще не
включенных в стандартную библиотеку libc (например, потому что систем-
системный вызов Linux еще не поддерживается библиотекой), однако, они непри-
непригодны для определения интерфейсных процедур для системных вызовов,
принимающих более шести параметров (не считая номера системного вызо-
вызова), или вызовов, возвращающих нестандартные значения.
Каждому макросу требуется 2 + 2 х п параметров, где п — количество пара-
параметров системного вызова. Первые два параметра определяют тип возвра-
возвращаемого значения и имя системного вызова, а каждая следующая пара пара-
параметров задает тип и имя соответствующего параметра системного вызова.
Например, интерфейсная процедура системного вызова fork о может быть
сгенерирована макросом:
_syscallO(int, fork)
А интерфейсная процедура системного вызова write () может быть сгенери-
сгенерирована макросом:
_syscall3(int,write,int,fd,const char *,buf,unsigned int,count)
В последнем случае макрос возвращает следующий код:
int write(int fd,const char * buf,unsigned int count)
{
long res;
asm ("int $0x80"
: "=a" ( res)
: " ( NR_write), "b" ((long)fd),
"c" ((long)buf), "d" ((long)count));
if ((unsigned long) res >= (unsigned long)-129) {
errno = - res;
res = -1;
}
return (int) res;
}
Макрос NRwrite получен из второго параметра макроса _syscaii3; он рас-
расширяется в номер системного вызова write (). При компиляции этой функции
будет сгенерирован следующий ассемблерный код:
write:
pushl %ebx ; положить ebx в стек
movl 8(%esp), %ebx ; занести первый параметр в ebx
movl 12(%esp), %ecx ; занести второй параметр в есх
movl 16(%esp), %edx ; занести третий параметр в edx
movl $4, %еах ; занести NR_write в еах
int
$0x80 ; сделать системный вызов
cmpl $-125, %еах ; проверить код возврата
jbe .L1 ; если ошибки нет, выполнить переход
negl %eax ; дополнить значение еах
movl %еах, errno ; поместить результат в errno
movl $-1, %еах ; записать -1 в еах
.LI: popl %ebx ; снять ebx со стека
ret ; возвратить управление вызвавшей программе
Обратите внимание, как параметры функции write () загружаются в регистры
процессора до выполнения инструкции int $0x8о. Значение, возвращаемое в
регистре еах, должно быть интерпретировано как код ошибки, когда оно ле-
лежит в диапазоне от -1 до -129 (ядро предполагает, что максимальный код
ошибки, определенный в файле include/generic/errno.h, равен 129). Если это
действительно так, интерфейсная процедура записывает значение -еах в пе-
переменную errno и возвращает -1. В противном случае она возвращает значе-
значение еах.
ГЛАВА 11
Сигналы
Сигналы были введены в первых Unix-системах для реализации взаимодейст-
взаимодействия между процессами в режиме пользователя. Кроме того, ядро с помощью
сигналов уведомляет процессы о системных событиях. Сигналы существуют
уже 30 лет и почти не изменились.
В первых разделах этой главы подробно рассматривается обработка сигналов
ядром Linux; затем мы обсуждаем системные вызовы, позволяющие процес-
процессам обмениваться сигналами.
Роль сигналов
Сигнал — это очень короткое сообщение, которое может быть послано про-
процессу или группе процессов. Информация, передаваемая процессу, как пра-
правило, сводится к номеру, идентифицирующему сигнал. В стандартных сигна-
сигналах нет места аргументам, тексту сообщения или иной сопутствующей ин-
информации.
Для идентификации сигналов используется набор макросов, имена которых
начинаются с префикса sig. В предыдущих главах мы уже упоминали их не-
несколько раз. Например, в главе 3 речь шла о макросе sigchld. Этот макрос,
расширяющийся в Linux в число 17, соответствует идентификатору сигнала,
посылаемого процессу-родителю, когда его потомок приостанавливается или
завершает работу. Макрос sigsegv, расширяющийся в число 11, упоминался в
главе 9. Он соответствует идентификатору сигнала, посылаемого процессу,
когда он делает недопустимую ссылку на ячейку памяти.
Сигнал служит двум основным целям:
□ уведомляет процесс о том, что произошло определенное событие;
□ заставляет процесс выполнить обработчик сигнала — функцию, включен-
включенную в код процесса.
Конечно, эти цели не являются взаимно исключающими, поскольку процесс
часто должен реагировать на событие, выполняя некоторую процедуру.
Табл. 11.1 содержит 31 сигнал, обрабатываемый в Linux 2.6 в архитектуре
80x86 (некоторые сигналы, такие как sigchld или sigstop, не зависят от ар-
архитектуры; другие же, например sigstkflt, определены только в конкретных
архитектурах). Смысл действий, предпринимаемых по умолчанию, раскрыва-
раскрывается в следующем разделе.
Таблица 11.1. Сигналы в Linux/i386
Номер Имя сигнала ^ умолчанию КомментаРий P0SIX
1 sighup Завершить Отключение терминала, управляю- Да
выполнение щего процессом
2 sigint Завершить Прерывание с клавиатуры Да
выполнение
3 sigquit Выполнить дамп Команда об окончании работы, Да
выданная с клавиатуры
4 sigill Выполнить дамп Недопустимая инструкция Да
5 sigtrap Выполнить дамп Точка останова для отладки Нет
6 sigabrt Выполнить дамп Аварийное завершение Да
6 sigiot Выполнить дамп Эквивалентен sigabrt Нет
7 sigbus Выполнить дамп Ошибка шины Нет
8 sigfpe Выполнить дамп Исключение при операции Да
с плавающей точкой
9 sigkill Завершить Принудительное завершение Да
выполнение процесса
10 SIGUSR1 Завершить Доступен процессам Да
выполнение
11 sigsegv Выполнить дамп Недопустимая ссылка на ячейку Да
памяти
12 SIGUSR2 Завершить Доступен процессам Да
выполнение
13 sigpipe Завершить Запись в канал, у которого нет Да
выполнение читающих процессов
Таблица 11.1 (окончание)
Номер Имя сигнала ^И молчанию Комментарий POSIX
14 sigalrm Завершить Таймер реального времени Да
выполнение
15 sigterm Завершить Завершение процесса Да
выполнение
16 sigstkflt Завершить Ошибка стека сопроцессора Нет
выполнение
17 sigchld Игнорировать Процесс-потомок остановился, Да
завершил выполнение или получил
сигнал, если выполнялось его
отслеживание
18 sigcont Продолжить Возобновить выполнение, если про- Да
цесс был остановлен
19 sigstop Остановить Остановить выполнение процесса Да
20 sigtstp Остановить Остановка процесса с терминала Да
21 sigttin Остановить Фоновый процесс затребовал ввод Да
22 sigttou Остановить Фоновый процесс затребовал вывод Да
23 sigurg Игнорировать Неотложная ситуация на сокете Нет
24 sigxcpu Выполнить дамп Превышен лимит времени Нет
процессора
25 sigxfsz Выполнить дамп Превышен максимальный размер Нет
файла
26 sigvtalrm Завершить Виртуальный таймер Нет
выполнение
27 sigprof Завершить Профилирующий таймер Нет
выполнение
28 sigwinch Игнорировать Изменение размера окна Нет
29 sigio Завершить Теперь ввод/вывод возможен Нет
выполнение
29 sigpoll Завершить Эквивалентен sigio Нет
выполнение
30 sigpwr Завершить Сбой питания Нет
выполнение
31 sigsys Выполнить дамп Недопустимый системный вызов Нет
31 sigunused Выполнить дамп Эквивалентен sigsys Нет
В дополнение к обычным сигналам, приведенным в этой таблице, стандарт
POSIX определяет новый класс сигналов, так называемые сигналы реального
времени. В Linux их номера находятся в диапазоне от 32 до 64. Они принци-
принципиально отличаются от обычных сигналов, потому что всегда ставятся в оче-
очередь так, что процесс принимает их все. Обычные сигналы того же типа в
очередь не ставятся: если обычный сигнал посылается несколько раз подряд,
процессу доставляется только один сигнал. Хотя ядро Linux не пользуется
сигналами реального времени, оно полностью поддерживает стандарт POSIX
при помощи специальных системных вызовов.
Ряд системных вызовов позволяет программистам посылать сигналы и опре-
определять реакцию процессов на получаемые сигналы. Эти системные вызовы
перечислены в табл. 11.2, а их поведение подробно описано в разд. "Систем-
"Системные вызовы, связанные с обработкой сигналов" далее в этой главе.
Таблица 11.2. Наиболее важные системные вызовы,
связанные с обработкой сигналов
Системный вызов Описание
kill () Послать сигнал группе потоков
tkiii () Послать сигнал процессу
tgkill () Послать сигнал процессу в конкретной группе потоков
sigaction () Изменить действие, ассоциированное с сигналом
signal () Аналогичен вызову sigaction ()
sigpending () Проверить, имеются ли необработанные сигналы
sigprocmask () Модифицировать множество заблокированных сигналов
sigsuspend () Ждать сигнал
rt_sigaction () Изменить действие, ассоциированное с сигналом реального
времени
rt_sigpending () Проверить, имеются ли необработанные сигналы реального
времени
rt_sigprocmask () Модифицировать множество заблокированных сигналов
реального времени
rt_sigqueueinfо () Послать сигнал реального времени группе потоков
rt_sigsuspend () Ждать сигнал реального времени
rt_sigtimedwait () Аналогичен вызову rt_sigsuspend ()
Важной особенностью сигналов является то, что они могут быть посланы
процессу в любой момент, и его состояние обычно непредсказуемо. Сигналы,
посланные приостановленному процессу, должны быть сохранены ядром,
пока выполнение процесса не возобновится. Блокирование сигнала (описан-
(описанное позже) означает, что доставка сигнала не должна производиться, пока
блокирование не будет отменено, что усугубляет проблему сигналов, отправ-
отправленных до того, как они могут быть доставлены.
Поэтому ядро различает две фазы передачи сигнала:
□ генерирование сигнала— ядро обновляет некоторую структуру данных
процесса-адресата для обозначения того факта, что сигнал послан;
□ доставка сигнала— ядро заставляет процесс-адресат отреагировать на
сигнал, т. е. изменить свое состояние, или вызвать указанный обработчик
сигнала, или сделать и то и другое.
Каждый сгенерированный сигнал может быть доставлен не более одного
раза. Сигналы являются невосстанавливаемыми ресурсами, т. е. после дос-
доставки сигнала вся информация о предыдущем сигнале, имеющаяся в деск-
дескрипторе процесса, утрачивается.
Сгенерированные, но еще не доставленные сигналы, называются висящими.
В каждый момент времени у процесса может быть только один висящий сиг-
сигнал данного типа. Следующие висящие сигналы того же типа, отправленные
тому же процессу, не выстраиваются в очередь, а просто отбрасываются.
С сигналами реального времени дело обстоит иначе: возможно наличие не-
нескольких висящих сигналов одного типа.
Вообще говоря, сигнал может оставаться висящим непредсказуемо долго.
Необходимо принимать во внимание следующие факторы:
□ сигналы, как правило, доставляются только текущему процессу (то есть
Процессу current);
□ сигналы определенного типа могут быть избирательно заблокированы
процессом (см. разд. "Модификация набора заблокированных сигналов"
далее в этой главе). В таком случае процесс не примет сигнал, пока не от-
отменит блокировку;
□ когда процесс выполняет обработчик сигнала, он обычно маскирует соот-
соответствующий сигнал, т. е. автоматически блокирует его, пока функция-
обработчик не завершит выполнение. Таким образом, обработчик сигнала
не может быть прерван другим поступлением обрабатываемого сигнала, и
функция не должна быть реентерабельной.
Хотя работа с сигналами интуитивно понятна, ее реализация в ядре достаточ-
достаточно сложна. Ядро должно:
□ запомнить, какие сигналы заблокированы, какими процессами;
□ при переключении из режима ядра в режим пользователя проверять, по-
поступил ли сигнал для процесса. Это происходит почти при каждом преры-
прерывании от таймера (примерно каждую миллисекунду);
□ определить, можно ли проигнорировать сигнал. Это происходит, когда
удовлетворены все следующие условия:
• процесс-адресат не отслеживается другим процессом (флаг ptptraced
в поле ptrace дескриптора процесса равен ОI;
• сигнал не блокирован принимающим процессом;
• сигнал игнорируется принимающим процессом (либо потому что про-
процесс игнорирует его явным образом, либо потому что процесс не изме-
изменил действие сигнала по умолчанию, которое заключается в игнориро-
игнорировании);
□ обработать сигнал, для чего, возможно, потребуется переключить процесс
на выполнение функции-обработчика в произвольной точке процесса, а
затем восстановить контекст выполнения процесса по окончании работы
функции.
В дополнение к сказанному, операционная система Linux должна учитывать
различную семантику сигналов в BSD и System V. Более того, она должна
соответствовать объемистым требованиям POSIX.
Действия, выполняемые при доставке сигнала
Процесс может отреагировать на сигнал трояко:
1. Явно проигнорировать сигнал.
2. Выполнить ассоциированное с сигналом действие по умолчанию
(табл. 11.1). Это действие, предопределенное ядром, зависит от типа сиг-
сигнала и может быть следующим:
• завершить выполнение — процесс завершается (уничтожается);
• выполнить дамп— процесс завершается (уничтожается), и, если воз-
возможно, создается файл core с контекстом выполнения процесса. Этот
файл может быть использован при отладке;
• игнорировать — сигнал игнорируется;
• остановить — процесс останавливается, т. е. переводится в состояние
task_stopped (см. главу 3);
• продолжить— если процесс был остановлен (taskstopped), он
переводится в состояние taskrunning.
3. Принять сигнал, вызвав соответствующую функцию-обработчик сигнала.
1 Если процесс принимает сигнал, когда отслеживается его выполнение, ядро останавливает его и
уведомляет отслеживающий процесс, отправляя ему сигнал SIGCHLD. В свою очередь, отслежи-
отслеживающий процесс может возобновить выполнение отслеживаемого с помощью сигнала SIGCONT.
Обратите внимание, что заблокировать сигнал— это не то же самое, что
проигнорировать его. Пока сигнал блокирован, он не доставляется; доставка
происходит только после снятия блокировки. Игнорируемый сигнал всегда
доставляется, но дальнейшие действия не предпринимаются.
Сигналы sigkill и sigstop нельзя игнорировать, обрабатывать специальной
функцией или блокировать, а их действия по умолчанию обязательно должны
быть выполнены. Таким образом, сигналы sigkill и sigstop позволяют поль-
пользователю с соответствующими привилегиями завершать или приостанавли-
приостанавливать любой процесс2, независимо от того, какую защиту установила програм-
программа, которую он выполняет.
Сигнал является фатальным для данного процесса, если доставка этого сиг-
сигнала приводит к уничтожению процесса ядром. Сигнал sigkill всегда фата-
фатален. Кроме того, каждый сигнал, действием которого по умолчанию является
"Завершить выполнение" и который не обрабатывается процессом с по-
помощью специальной функции, является фатальным для этого процесса. Сле-
Следует, однако, отметить, что сигнал, обрабатываемый процессом так, что
функция-обработчик завершает процесс, фатальным не является. Ведь про-
процесс сам выбирает свое уничтожение, а не уничтожается ядром.
Сигналы POSIX
и многопоточные приложения
Стандарт POSIX 1003.1 устанавливает ряд строгих ограничений на обработку
сигналов в многопоточных приложениях:
□ обработчики сигналов должны быть общими для всех потоков такого при-
приложения. Тем не менее каждый поток должен иметь собственную маску
для висящих и заблокированных сигналов;
□ библиотечные функции kill о и sigqueueo, определяемые в POSIX (см.
разд. "Системные вызовы, связанные с обработкой сигналов" далее в
этой главе), должны отправлять сигналы многопоточному приложению в
целом, а не конкретному потоку. То же самое справедливо и в отношении
сигналов, генерируемых ядром (таких как sigchld, sigint или sigquit);
□ каждый сигнал, отправленный многопоточному приложению, будет дос-
доставлен только одному потоку, произвольно выбранному ядром из числа
потоков, не заблокировавших этот сигнал;
2 Из этого правила существуют два исключения. Нельзя отправить сигнал процессу 0 (swapper), и
сигналы, отправляемые процессу 1 (init), всегда отбрасываются, если не обрабатываются явно. Сле-
Следовательно, процесс 0 невозможно уничтожить, а процесс 1 уничтожается только по окончании
работы программы init.
□ если многопоточному приложению отправлен фатальный сигнал, ядро
уничтожит все потоки приложения, а не только тот, которому сигнал был
доставлен.
Чтобы соответствовать стандарту POSIX, ядро Linux 2.6 реализует многопо-
многопоточное приложение как множество облегченных процессов, принадлежащих
одной группе (см. главу 3).
В этой главе термин "группа потоков" относится к любой такой группе, даже
если она состоит из одного (обычного) процесса. Например, когда мы утвер-
утверждаем, что kill о может послать сигнал группе потоков, мы подразумеваем,
что этот системный вызов может также послать сигнал и обычному процессу.
Мы будем термином "процесс" обозначать либо обычный, либо облегченный
процесс, т. е. конкретный член группы потоков.
Висящий сигнал является частным, если он отправлен конкретному процессу;
сигнал, отправленный целой группе, является совместно используемым.
Структуры данных,
ассоциированные с сигналами
Для каждого процесса в системе ядро должно отслеживать, какие сигналы в
данный момент висят или замаскированы. Ядро также должно отслеживать,
как каждая группа потоков будет обрабатывать сигналы. Для этих целей ядро
использует несколько структур, доступных через дескриптор процесса. Са-
Самые важные из них изображены на рис. 11.1.
Поля дескриптора процесса, имеющие отношение к обработке сигнала, пере-
перечислены в табл. 11.3.
Таблица 11.3. Поля дескриптора процесса, имеющие отношение
к обработке сигнала
Тип Имя Описание
struct signalstruct * signal Указатель на дескриптор сигнала, при-
принадлежащий процессу
struct sighand_struct * sighand Указатель на дескриптор обработчика
сигналов
sigset_t blocked Маска заблокированных сигналов
sigsett realblocked Временная маска заблокированных сиг-
сигналов (применяется в системном вызове
rt_sigtimedwait ())
struct sigpending pending Структура данных, содержащая частные
висящие сигналы
Таблица 11.3 (окончание)
Тип Имя Описание
unsigned long sas_ss_sp Адрес альтернативного стека обработчи-
обработчика сигнала
size_t sas_ss_size Размер альтернативного стека обработ-
обработчика сигнала
int (*) (void *) notifier Указатель на функцию, используемую
драйвером устройства для блокировки
некоторых сигналов процесса
void * notif ierdata Указатель на данные, которые могут по-
понадобиться уведомляющей функции (см.
предыдущее поле таблицы)
sigset_t * notif ier_mask Битовая маска сигналов, заблокирован-
заблокированных драйвером устройства при помощи
уведомляющей функции
Рис. 11.1. Важнейшие структуры, связанные с обработкой сигналов
Поле blocked хранит сигналы, замаскированные процессом на данный мо-
момент. Это массив sigsett, имеющий по одному биту на каждый тип сигнала:
typedef struct {
unsigned long sig[2];
} sigset_t;
Поскольку каждое число типа unsigned long включает в себя 32 бита, макси-
максимальное количество сигналов, объявляемых в Linux, равно 64 (это значение
определяет макрос nsig). Поскольку сигнала с номером 0 нет, номер сигнала
равен индексу соответствующего бита в переменной sigsett плюс единица.
Номера с 1 по 31 соответствуют сигналам, приведенным в табл. 11.1, а номе-
номера с 32 по 64 — сигналам реального времени.
Дескриптор сигнала и дескриптор обработчика сигналов
Поле signal дескриптора процесса указывает на дескриптор сигнала —
структуру signaistruct, которая отслеживает совместно используемые ви-
висящие сигналы. На самом деле, дескриптор сигнала включает в себя и поля,
не имеющие непосредственного отношения к обработке сигналов, например,
rlim— массив ограничений на ресурсы для каждого процесса или поля pgrp
и session, в которых хранятся идентификаторы процессов для лидера группы
и, соответственно, лидера сессии данного процесса. Фактически, как уже бы-
было сказано в главе 3, дескриптор сигнала совместно используется всеми про-
процессами, принадлежащими одной группе потоков, т. е. всеми процессами,
созданными системным вызовом clone о с установленным флагом
clonethread. Таким образом, дескриптор сигнала имеет поля, которые долж-
должны быть идентичными для каждого процесса в одной группе потоков.
Поля дескриптора сигнала, имеющие отношение к обработке сигналов, пере-
перечислены в табл. 11.4.
Таблица 11.4. Поля дескриптора сигнала, имеющие отношение
к обработке сигналов
Тип Имя Описание
atomic_t count Счетчик обращений дескриптора сигнала
atomic_t live Количество "живых" процессов в группе
потоков
wait_queue_head_t wait_chldexit Очередь ожидания для процессов, при-
приостановленных системным вызовом
wait4 ()
struct task_struct * curr_target Дескриптор последнего процесса в груп-
группе, получившего сигнал
struct sigpending shared_pending Структуры, содержащие совместно
используемые висящие сигналы
Таблица 11.4 (окончание)
Тип Имя Описание
int group_exit_code Код завершения процесса для группы
потоков
struct task_struct * group_exit_task Используется при уничтожении целой
группы потоков
int notify_count Используется при уничтожении целой
группы потоков
int group_stop_count Используется при остановке целой груп-
группы потоков
unsigned int flags Флаги, используемые при доставке сиг-
сигналов, изменяющих состояние процесса
Помимо дескриптора сигнала каждый процесс обращается к дескриптору об-
обработчика сигналов. Это структура sighandstruct, описывающая, как каж-
каждый сигнал должен быть обработан группой потоков. Ее поля перечислены
в табл. 11.5.
Таблица 11.5. Поля дескриптора обработчика сигналов
Тип Имя Описание
atomic_t count Счетчик обращений дескриптора обработчика
сигналов
struct k_sigaction [64] action Массив структур, описывающих действия, кото-
которые должны быть выполнены при доставке сиг-
сигналов
spinlock_t siglock Спин-блокировка, защищающая дескриптор
сигнала и дескриптор обработчика сигналов
Как было сказано в главе 3, дескриптор обработчика сигналов может быть
совместно использован несколькими процессами, созданными системным
вызовом clone о с установленным флагом clonesighand; при этом поле count
этого дескриптора показывает количество процессов, совместно использую-
использующих данную структуру. В многопоточном приложении, удовлетворяющем
стандарту POSIX, все облегченные процессы в группе потоков работают
с одним дескриптором обработчика сигналов.
Структура sigaction
В некоторых архитектурах сигналу присваиваются свойства, о которых из-
известно только ядру. Свойства сигнала хранятся в структуре ksigaction, ко-
торая содержит как свойства, скрытые от процесса режима пользователя, так
и структуру sigaction, включающую в себя свойства, видимые процессу.
В архитектуре 80><86 процессам режима пользователя видны все свойства
сигнала. В этом случае структура ksigaction просто сводится к структуре sa
типа sigaction, которая состоит из следующих полей3:
□ sahandier— задает тип выполняемого действия. Его значениями могут
быть указатель на обработчик сигнала, константа sigdfl (to есть 0), опре-
определяющая, что должно быть выполнено действие по умолчанию, и кон-
константа SIGIGN (то есть 1), указыващая, что сигнал должен быть проигно-
проигнорирован;
□ saf lags — набор флагов определяет, как должен быть обработан сигнал.
Некоторые из флагов перечислены в табл. 11.64;
□ samask— переменная типа sigsett задает сигналы, которые должны
быть замаскированы при выполнении обработчика сигнала.
Таблица 11.6. Флаги, определяющие, как должен быть обработан сигнал
Имя Описание
sa_nocldstop Относится только к sigchld. Не отправлять сигнал sigchld процес-
процессу-родителю, когда его потомок останавливается
sa_nocldwait Относится только к sigchld. He создавать зомби, когда процесс
завершается
sa_siginfo Предоставить обработчику сигналов дополнительную информацию
sa_onstack Использовать альтернативный стек для обработчика сигнала
sa_restart Автоматически заново запускать прерванные системные вызовы
sa_nodefer, He маскировать сигнал при выполнении обработчика сигналов
SA_NOMASK
sa_resethand, По окончании работы обработчика сигналов выполнить сброс в дей-
sa_oneshot ствие по умолчанию
Очереди висящих сигналов
Как видно из табл. 11.2, существует несколько системных вызовов, спо-
способных сгенерировать сигнал. Некоторые из них, такие как kill о и
3 Структура sigaction, применяемая в приложениях режима пользователя для передачи парамет-
параметров системным вызовам signal () и sigaction(), немного отличается от структуры, используе-
используемой ядром, хотя и содержит практически ту же информацию.
4 Исторически сложилось, что эти флаги имеют тот же префикс SA_, что и флаги дескриптора
irqaction (см. табл. 4.7 в главе 4). Несмотря на это, между двумя наборами флагов нет никакой
связи.
rtsigqueueinf о (), посылают сигнал целой группе потоков, в то время как
другие, например tkiiio и tgkiiio, посылают сигнал конкретному про-
процессу.
Таким образом, чтобы отслеживать висящие сигналы, ядро ассоциирует с
каждым процессом две очереди висящих сигналов:
□ очередь висящих совместно используемых сигналов с корнем в поле
sharedpending дескриптора сигнала содержит висящие сигналы целой
группы потоков;
□ очередь висящих частных сигналов с корнем в поле pending дескриптора
процесса содержит висящие сигналы конкретного (облегченного) про-
процесса.
Очередь висящих сигналов фактически состоит из структуры sigpending, оп-
определяемой следующим образом:
struct sigpending {
struct list_head list;
sigset_t signal;
}
Поле signal является битовой маской, определяющей висящие сигналы, а по-
поле list ГОЛОВОЙ двунаправленного СПИСКа Структур sigqueue. ПОЛЯ ЭТОЙ
структуры перечислены в табл. 11.7.
Таблица 11.7. Поля структуры sigqueue
Тип Имя Описание
struct list_head list Указатели, используемые в списке очереди вися-
висящих сигналов
spinlock_t * lock Указатель на поле siglock дескриптора обработчи-
обработчика сигналов, соответствующего висящему сигналу
int flags Флаги структуры sigqueue
siginfo_t info Описание события, возбуждаемого сигналом
struct user_struct * user Указатель на структуру данных владельца процес-
процесса, отдельную для каждого пользователя
Структура siginfot занимает 128 байтов и хранит в себе информацию о кон-
конкретном сигнале. Она состоит из следующих полей:
□ sisigno — номер сигнала;
□ sierrno— код ошибки для инструкции, приведшей к возбуждению сиг-
сигнала, или 0, если ошибки не было;
□ sicode — код, идентифицирующий источник сигнала (табл. 11.8);
□ _sifieids — объединение, в котором хранится информация, специфичная
для типа сигнала. Например, структура siginfot, соответствующая сиг-
сигналу sigkill, записывает сюда PID (идентификатор процесса) и UID
(идентификатор пользователя) процесса-отправителя, а структура, соот-
соответствующая сигналу sigsegv, сохраняет в этом поле адрес ячейки, обра-
обращение к которой привело к отправке сигнала.
Таблица 11.8. Наиболее важные отправители сигналов
Кодовое имя Отправитель
SIJJSER Системный вызов kill () и raise ()
si_kernel Функция ядра общего назначения
SI_QUEUE Системный ВЫЗОВ sigqueue ()
si_timer Таймер (время истекло)
si_asyncio Асинхронная операция ввода/вывода (завершение операции)
si_tkill Системные вызовы tkill () и tgkill ()
Операции над сигнальными структурами
Для обработки сигналов ядро применяет несколько функций и макросов.
В приведенном далее описании set является указателем на переменную
sigsett, nsig — НОМер сигнала, a mask — маска, имеющая ТИП unsigned long.
□ sigemptyset (set) И sigfillset (set) — соответственно, сбрасывают И ус-
танавливают биты в переменной sigsett;
□ sigaddset (set, nsig) И sigdelset (set, nsig) — сбрасывают И устанавлива-
ют бит в переменной sigsett, соответствующий сигналу nsig. На практи-
практике sigaddset () сводится к оператору
set->sig[(nsig - 1) / 32] |= 1UL « ((nsig - 1) % 32);
a sigdeiset () — к оператору
set->sig[(nsig - 1) / 32] &= ~AUL « ((nsig - 1) % 32));
□ sigaddsetmask(set,mask) И sigdelsetmask (set,mask) — устанавливает все
биты переменной sigsett, для которых соответствующие биты маски
mask равны 1 или 0 соответственно. Эти функции можно использовать
только с сигналами, номера которых лежат между 1 и 32.
Функции сводятся к операторам
set->sig[0] |= mask;
И
set->sig[0] &= ~mask;
□ sigismember(set,nsig) — возвращает значение бита переменной sigsett,
соответствующего сигналу nsig. На практике эта функция сводится к опе-
оператору
return 1 & (set->sig[(nsig-1) / 32] » ((nsig-1) % 32));
□ sigmask(nsig) — возвращает индекс бита сигнала nsig. Иными словами,
если ядру понадобится сбросить, установить или проверить бит элемента
sigsett, соответствующий данному сигналу, оно может вычислить нуж-
нужный бит с помощью этого макроса;
sigandsets(d,si,s2), sigorsets(d,si,s2) И signandsets(d,si,s2) —
выполняют логическую операцию И, ИЛИ или НЕ-И, соответственно над
переменными sigsett, некоторые указывают параметры si и s2. Резуль-
Результат сохраняется в переменной sigsett, на которую указывает параметр d;
□ sigtestsetmask(set,mask) — возвращает 1, если установлен хотя бы один
из битов переменной sigsett, соответствующий битам, установленным в
маске mask; в противном случае возвращает 0. Эту функцию можно ис-
использовать только с сигналами, номера которых лежат между 1 и 32;
□ siginitset (set, mask) — инициализирует биты переменной sigsett, соот-
ветствующие сигналам от 1 до 32, битами из маски mask и сбрасывает би-
биты, соответствующие сигналам от 33 до 63;
О siginitsetinv(set,mask) — инициализирует биты переменной sigsett,
соответствующие сигналам от 1 до 32, битами, комплементарными битам
маски mask, и устанавливает биты, соответствующие сигналам от 33 до 63;
□ signaijpending(p) — возвращает 1 (истина), если процесс, идентифици-
идентифицируемый дескриптором *р, имеет незаблокированные висящие сигналы, и О
(ложь) в противном случае. Функция реализована в виде простой проверки
флага tifsigpending этого процесса;
□ recalc_sigpending_tsk(t) И recalc_sigpending () —первая функция прове-
проверяет, имеются ли висящие сигналы либо для процесса, идентифицируемо-
идентифицируемого дескриптором *t (с этой целью она анализирует поле t->pending->
signal), либо для всей группы, в которую входит процесс (с этой целью
анализируется поле t->signai->shared_pending->signai). Затем функция
должным образом устанавливает флаг tifsigpending в поле t->thread_
info->flags. ФуНКЦИЯ recalcsigpendingO Эквивалентна ВЫЗОВу фуНКЦИИ
recalc_sigpending_tsk(current);
□ rmf romqueue (mask, q) — удаляет ИЗ Очереди ВИСЯЩИХ СИГН8ЛОВ q ВИСЯЩИе
сигналы, определяемые битовой маской mask;
□ fiushsigqueue(q)— удаляет из очереди висящих сигналов q все висящие
сигналы;
□ fiushsigqueue(q) — удаляет все сигналы, посланные процессу, иденти-
идентифицируемому дескриптором *t. Это достигается путем сброса флага
tifsigpending в поле t->thread_info->f lags и двухкратного вызова функ-
функции flush_sigqueue () ДЛЯ очередей t->pending И t->signal->shared_
pending.
Генерирование сигнала
Сигналы генерируются многими функциями ядра. Первую фазу обработки
сигнала (описанную в разд. "Роль сигналов"ранее в этой главе) эти функции
выполняют, обновляя один или несколько дескрипторов процессов по мере
необходимости. Они не выполняют непосредственно вторую фазу, доставку
сигнала, но, в зависимости от типа сигнала и состояния процессов-
получателей, могут "разбудить" некоторые процессы и заставить их принять
сигнал.
Когда сигнал отправляется процессу либо ядром, либо другим процессом,
ядро генерирует его с помощью одной из функций, перечисленных в
табл. 11.9.
Таблица 11.9. Функции ядра, генерирующие сигнал для процесса
Имя Описание
send_sig () Посылает сигнал одному процессу
send_sig_info() Аналогична функции send_sig() с дополнительной инфор-
информацией в структуре siginfot
force_sig() Посылает сигнал, который не может быть явно проигнориро-
проигнорирован или блокирован процессом
force_sig_infо () Аналогична функции force_sig() с дополнительной инфор-
информацией в структуре siginfo_t
force_sig_specif ic () Аналогична функции f orce_sig (), но оптимизирована для
сигналов sigstop и sigkill
sys_tkili () Обработчик системного вызова tkill ()
sys_tgkill () Обработчик системного вызова tgkill ()
Все функции из табл. 11.9 заканчиваются вызовом функции specific_
sendsiginf о (), описанной в следующем разделе.
Когда сигнал посылается всей группе потоков либо ядром, либо другим про-
процессом, ядро генерирует его с помощью одной из функций, перечисленных
в табл. 11.10.
Таблица 11.10. Функции ядра, генерирующие сигнал для группы потоков
Имя Описание
send_group_sig_info() Посылает сигнал одной группе потоков, идентифицируемой
дескриптором одного из процессов, принадлежащих группе
kilipg () Посылает сигнал всем группам потоков в группе процессов
kill_pg_inf о () Аналогична функции killpgO с дополнительной информа-
информацией в структуре siginf o_t
kill_proc() Посылает сигнал одной группе потоков, идентифицируемой
идентификатором одного из процессов, принадлежащих
группе
kill_proc_info() Аналогична функции kill_proc() с дополнительной
информацией в структуре siginfot
syskill () Обработчик системного вызова kill ()
sys_rt_sigqueueinf о () Обработчик системного вызова rt_sigqueueinf о ()
Все функции из табл. 11.10 заканчиваются вызовом функции group_send_
siginfoO, описанной в разд. "Функция group send siginfoQ" далее в этой
главе.
Функция specific_send_sigjnfo()
Функция specif icsendsiginfo о посылает сигнал конкретному процессу.
Она принимает три параметра:
□ sig — номер сигнала;
□ info— либо адрес таблицы siginfo_t, либо одно из трех специальных
значений: 0 — сигнал был послан процессом режима пользователя, 1 —
сигнал был послан ядром, 2 — сигнал был послан ядром, и это sigstop или
sigkill;
□ t — указатель на дескриптор процесса-получателя.
Функция specif icsendsiginfo о должна быть вызвана с отключенными
локальными прерываниями и с уже полученной спин-блокировкой t->
sighand->sigiock. Она выполняет следующие действия:
1. Проверяет, игнорируется ли сигнал процессом. Если это так, возвращает 0
(сигнал не сгенерирован). Сигнал игнорируется, когда удовлетворены все
три условия:
• процесс не отслеживается (флаг ptptraced в поле t->ptrace сброшен);
• сигнал не заблокирован (функция sigismember (&t->blocked, sig) ВОЗ-
вращает 0);
• СИГНал Игнорируется либо ЯВНО (поле sa_handler структуры t->sighand->
action [sig-1] paBHO SIGIGN), либо KOCBeHHO (поле sahandler paBHO
SIG_DFL, И ПОСЛаН СИГНЗЛ SIGCONT, SIGCHLD, SIGWINCH ИЛИ SIGURG).
2. Проверяет, что сигнал не является сигналом реального времени (sig<32), и
другой экземпляр этого сигнала уже находится в очереди частных вися-
висящих сигналов процесса (функция sigismember (&t->pending. signal, sig)
возвращает 1). Если это действительно так, ничего делать не нужно, и
функция возвращает 0.
3. Вызывает функцию sendsignal (sig, info, t, &t->pending), чтобы ДОба-
вить сигнал в очередь висящих сигналов процесса. Эта функция подробно
описана в следующем разделе.
4. Если функция sendsignaio завершилась успешно, и сигнал не заблоки-
заблокирован (функция sigismember (&t->biocked, sig) возвращает 0), описываемая
функция вызывает функцию signaiwakeupo, чтобы уведомить процесс
о новом висящем сигнале. Вызванная функция выполняет следующие дей-
действия:
• устанавливает флаги tif_sigpending в поле t->thread_info->f lags;
• вызывает функцию trytowakeup () (см. главу 7), чтобы возобновить
выполнение процесса, если он находится в состоянии task_
interruptible, либо в состоянии task_stopped, причем послан сигнал
sigkill;
• если функция trytowakeupo возвратила 0, значит, процесс уже был
"разбужен". В таком случае функция signaiwakeupo проверяет, вы-
выполняется ли процесс на другом процессоре, и, если это так, посылает
тому процессору межпроцессорное прерывание, чтобы произошла пе-
перепланировка выполнения текущего процесса (см. главу 4). Поскольку
каждый процесс проверяет наличие висящих сигналов после возврата
управления от функции schedule о, межпроцессорное прерывание яв-
является гарантией того, что процесс-получатель заметит новый висящий
сигнал.
5. Возвращает 1 (сигнал успешно сгенерирован).
Функция send_signal()
Функция sendsignaio заносит новый элемент в очередь висящих сигналов.
Она принимает в качестве параметров номер сигнала sig, адрес info структу-
ры siginfot (или специальный код), адрес t дескриптора процесса-
получателя и адрес signals очереди висящих сигналов.
Функция выполняет следующие действия:
1. Если параметр info равен 2, значит, послан сигнал sigkill или sigstop, и
он был сгенерирован ядром с помощью функции force_sig_specific().
В таком случае описываемая функция переходит к шагу 9. Действия, яв-
являющиеся реакцией на эти сигналы, немедленно выполняются ядром, так
что функция может не заносить сигнал в очередь висящих сигналов.
2. Если количество висящих сигналов у владельца процесса (t->user->
sigpending) не превышает лимит ресурсов текущего процесса ((t->signai->
rlim[RLIMIT_SIGPENDING] .rlim_cur), фуНКЦИЯ выделяет Структуру sigqueue
для нового экземпляра сигнала:
q = kmem_cache alloc(sigqueue_cachep, GFP_ATOMIC);
3. Если количество висящих сигналов у владельца процесса слишком велико,
или попытка выделения памяти на предыдущем шаге закончилась неудач-
неудачно, функция переходит к шагу 9.
4. Увеличивает количество висящих сигналов владельца (t->user->
sigpending) и счетчик ссылок структуры данных (отдельной у каждого
пользователя), на которую указывает поле t->user.
5. Заносит структуру sigqueue в очередь висящих сигналов signals:
list_add_tail(&q->list, &signals->list);
6. Заполняет таблицу siginfot В структуре sigqueue:
if ((unsigned long)info ==0) {
q->info.si signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_USER;
q->info._sifields._kill._pid = current->pid;
q->info._sifields._kill._uid = current->uid;
} else if ((unsigned long)info == 1) {
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_KERNEL;
q->info._sifields._kill._pid = 0;
q->info._sifields._kill._uid = 0;
} else
copy_siginfo(&q->info, info);
Функция copysiginfoo копирует таблицу siginfot, переданную при вы-
вызове.
7. Устанавливает бит, соответствующий сигналу, в битовой маске очереди:
sigaddset(&signals->signal, sig);
8. Возвращает 0 (сигнал успешно добавлен в очередь висящих сигналов).
9. Если функция на этом шаге, значит, сигнал не будет занесен в очередь
висящих сигналов, потому что висящих сигналов уже очень много, или
для структуры sigqueue нет свободной памяти, или сигнал немедленно
обрабатывается ядром. Если это сигнал реального времени, и он послан
функцией ядра (то есть его обязательно нужно было поставить в очередь),
функция возвращает код ошибки -eagain:
if (sig>=32 && info && (unsigned long) info != 1 &&
info->si_code != SIJJSER)
return -EAGAIN;
10. Устанавливает бит, соответствующий сигналу, в битовой маске очереди:
sigaddset(&signals->signal, sig);
11. Возвращает 0. Даже если сигнал не был добавлен в очередь, соответст-
соответствующий бит в маске висящих сигналов был установлен.
Важно позволить процессу-получателю принять сигнал, даже если не на-
нашлось места для нового элемента очереди сигналов. Предположим, напри-
например, что процесс занимает слишком много памяти. Ядро должно обеспечить
успешное выполнение системного вызова kill о, даже если нет свободной
памяти. В противном случае у системного администратора не будет возмож-
возможности снять мешающий процесс и восстановить функционирование системы.
Функция group_send_sigjnfo()
Функция group_send_sig_info() отправляет сигнал всей группе потоков. Она
принимает три параметра: номер сигнала sig, адрес info структуры siginfo_t
(или одно из специальных значений 0, 1 или 2) и адрес р дескриптора про-
процесса.
Функция выполняет следующие действия:
1. Проверяет корректность параметра sig:
if (sig < 0 || sig > 64)
return -EINVAL;
2. Если сигнал был послан процессом режима пользователя, функция прове-
проверяет, разрешена ли эта операция. Сигнал будет доставлен только при вы-
выполнении хотя бы одного из следующих условий:
• владелец процесса, пославшего сигнал, имеет соответствующее разре-
разрешение (как правило, это просто означает, что сигнал был послан сис-
системным администратором, см. главу 20);
• был послан сигнал sigcont, и процесс-получатель находится в одном
сеансе с процессом-отправителем;
• оба процесса принадлежат одному пользователю.
Если процессу режима пользователя не разрешено посылать сигнал, функ-
функция возвращает код -eperm.
3. Если параметр sig равен 0, функция немедленно возвращает управление,
не генерируя сигнал:
if (!sig || !p->sighand)
return 0;
Поскольку ноль не является номером сигнала, он используется процессом-
отправителем для проверки наличия у себя привилегий, достаточных для
отправки сигнала группе потоков. Функция также возвращает управление,
если процесс-получатель в данный момент уничтожается; об этом она уз-
узнает, проверяя, освобожден ли дескриптор обработчика сигналов, принад-
принадлежащего этому процессу.
4. Получает спин-блокировку p->sighand->sigiock и отключает локальные
прерывания.
5. Вызывает функцию handiestopsignaio, которая проверяет тип сигнала,
поскольку сигналы некоторых типов могут аннулировать другие висящие
сигналы, посланные группе потоков. Вызванная функция выполняет сле-
следующие действия:
• если группа потоков в данный момент уничтожается (флаг
signalgroupexit в поле flags дескриптора сигнала установлен),
функция возвращает управление;
• если параметр sig определяет сигнал sigstop, sigtstp, sigttin или
sigttou, функция вызывает функцию rmf romqueue (), чтобы удалить
сигнал sigcont из очереди висящих совместно используемых сигналов
p->signai->shared_pending и из очередей частных сигналов всех членов
группы;
• если параметр sig определяет сигнал sigcont, функция вызывает функ-
функцию rm_f rom_queue (), ЧТОбы удалИТЬ СИГНаЛЫ SIGSTOP, SIGTSTP, SIGTTIN
и sigttou (если таковые имеются) из очереди висящих совместно ис-
используемых сигналов p->signai->shared_pending. Затем функция удаля-
ет те же сигналы из очередей висящих частных сигналов всех процессов,
входящих в группу, и возобновляет выполнение этих процессов:
rm_from_queue(ОхООЗсОООО, &p->signal->shared_pending);
t = р;
do {
rm_from_queue(ОхООЗсОООО, &t->pending);
try_to_wake_up(t, TASK_STOPPED, 0);
t = next_thread(t);
} while (t != p);
Маска ОхООЗсОООО выделяет четыре стоп-сигнала. При каждой итерации
макрос nextthread возвращает адрес дескриптора очередного облегчен-
облегченного процесса в группе (см. главу ЗM.
6. Проверяет, проигнорирует ли группа данный сигнал. Если это так, воз-
возвращает 0 (успех). Сигнал игнорируется, если удовлетворены все три ус-
условия, упомянутые в разд. "Роль сигналов"ранее в этой главе.
7. Проверяет, что сигнал не является сигналом реального времени (sig<32),
и другой экземпляр этого сигнала уже находится в очереди висящих со-
совместно используемых сигналов группы потоков. Если это так, ничего не
нужно делать, и функция возвращает 0 (успех):
if (sig<32 && sigismember(&p->signal->shared_pending.signal,sig))
return 0;
8. Вызывает функцию sendsignaio, чтобы поставить сигнал в очередь ви-
висящих СОВМеСТНО ИСПОЛЬЗуеМЫХ СИГНаЛОВ. ЕСЛИ фуНКЦИЯ send_signal()
возвратит ненулевой код ошибки, описываемая функция завершит работу
с тем же кодом ошибки.
9. Вызывает функцию group_complete_signal(), чтобы разбуДИТЬ ОДИН ИЗ
облегченных процессов в группе (см. ниже).
10. Освобождает спин-блокировку p->sighand->sigiock и включает локаль-
локальные прерывания.
11. Возвращает 0 (успех).
Функция groupcompietesignai о перебирает процессы в группе в поисках
процесса, который может принять новый сигнал.
5 На самом деле код функции сложнее, чем в приведенных здесь фрагментах, потому что функция
handlestopsignal () принимает во внимание редкий случай, когда сигнал SIGCONT обрабаты-
обрабатывается, а также учитывает конкуренцию, возникающую, если сигнал SIGCONT послан в то время,
когда все процессы в группе останавливаются (но еще не остановлены).
Процесс будет выбран, если он удовлетворяет всем следующим условиям:
□ процесс не блокирует сигнал;
□ процесс не находится ни в одном из следующих состояний: exitzombie,
EXIT_DEAD, TASK_TRACED И TASK_STOPPED (в ПОрЯДКе ИСКЛЮЧеНИЯ ПрОЦеСС МО-
жет быть в состоянии task_traced или task_stopped, если сигнал является
сигналом sigkill);
П процесс не уничтожается, т. е. его флаг pfjexiting не установлен;
□ либо процесс в данный момент выполняется каким-нибудь процессором,
либо его флаг tifsigpending еще не установлен. На практике нет никако-
никакого смысла будить процесс, у которого имеются висящие сигналы: вообще
говоря, это сделал управляющий тракт ядра, установивший флаг
tifsigpending. С другой стороны, если процесс выполняется, его следует
уведомить о новом висящем сигнале.
Группа потоков может включать в себя много процессов, удовлетворяющих
этим условиям. Функция выбирает один из них, руководствуясь следующими
соображениями:
□ если процесс, идентифицируемый переменной р (адресом дескриптора,
переданным в качестве параметра функции groupsendsiginfoo), удов-
удовлетворяет всем перечисленным требованиям и, следовательно, может при-
принять сигнал, функция выбирает его;
□ в противном случае функция ищет подходящий процесс, перебирая эле-
элементы группы, начав с процесса, который принял последний сигнал, по-
посланный группе (p->signal->curr_target).
ЕСЛИ фуНКЦИИ groupcompletesignal () удаеТСЯ НаЙТИ ПОДХОДЯЩИЙ Процесс,
она обеспечивает доставку сигнала выбранному процессу. Во-первых, она
проверяет, является ли сигнал фатальным. В этом случае уничтожается вся
группа путем отправки сигналов sigkill каждому облегченному процессу в
группе. Если же сигнал не фатален, функция вызывает функцию signai_
wakeupo, чтобы уведомить выбранный процесс о новом висящем сигнале
(см. шаг 4 в разд. "Функция specific sendsiginfoQ "ранее в этой главе).
Доставка сигнала
Мы предполагаем, что ядро обнаружило поступление сигнала и вызвало одну
из функций, описанных в предыдущих разделах, чтобы подготовить дескрип-
дескриптор процесса, который, предположительно, будет принимать сигнал. Однако
если данный процесс не выполнялся в тот момент, ядро отложило доставку
сигнала. Сейчас мы обсудим действия, выполняемые ядром для обеспечения
обработки процессом висящих сигналов.
Как было сказано в главе 4, ядро проверяет флаг tifsigpending процесса
прежде, чем разрешит процессу возобновить работу в режиме пользователя.
Так ядро проверяет наличие висящих сигналов всякий раз, когда оно завер-
завершает обработку прерывания или исключения.
Чтобы обработать незаблокированные висящие сигналы, ядро вызывает
функцию dosignai (), которая принимает два параметра:
□ regs — адрес области стека, в которой сохраняется содержимое регистров
режима пользователя текущего процесса;
□ oidset — адрес переменной, в которой функция сохранит битовый массив,
представляющий собой маску заблокированных сигналов. Этот параметр
равен null, если сохранять массив-маску нет необходимости.
При описании функции dosignaio мы основное внимание уделили общим
механизмам доставки сигналов. Реальный код перегружен обработкой ситуа-
ситуаций параллельного обращения и других специальных случаев, например, "за-
"зависания" системы, генерирования дампов, остановки и уничтожения целых
групп потоков и т. д. Мы не станем вдаваться в обсуждение этих подроб-
подробностей.
Как уже было сказано, функция dosignaio обычно вызывается, лишь когда
процессор собирается возвратиться в режим пользователя. По этой причине,
если обработчик прерывания вызывает функцию dosignai (), она просто воз-
возвращает управление:
if ( (regs->xcs & 3) != 3)
return 1;
Если параметр oidset равен null, функция инициализирует его адресом поля
current->blocked:
if (!oidset)
oidset = ¤t->blocked;
Важнейшей частью функции dosignaio является цикл, многократно вызы-
вызывающий функцию dequeuesignaio, пока не будут обработаны все незабло-
незаблокированные висящие сигналы в очередях частных и совместно используемых
висящих сигналов. Код возврата функции dequeuesignai о хранится в ло-
локальной переменной signr. Если он равен 0, это означает, что в очередях не
осталось ни одного висящего сигнала, и функция dosignaio может закон-
закончить работу. Если возвращается ненулевое значение, какой-то висящий сиг-
сигнал ждет обработки. Функция dequeuesignai о вызывается снова после того,
как функция dosignai () обработает текущий сигнал.
Функция dequeuesignaio сначала рассматривает все сигналы в очереди ча-
частных висящих сигналов, начиная с сигнала с наименьшим номером, а за-
тем — сигналы в очереди совместно используемых сигналов. Она обновляет
структуры данных, отмечая тот факт, что сигнал больше не является вися-
висящим, и возвращает его номер. Эта задача включает в себя сброс соответст-
соответствующего бита В ПОЛе current->pending.signal ИЛИ current->signal->
sharedpending. signal И ВЫЗОВ функции recalcsigpending (), обновляющей
значение флага tif_sigpending.
Рассмотрим, как функция dosignaio обрабатывает висящий сигнал, номер
которого возвращен функцией dequeuesignaio. Вначале она проверяет, от-
отслеживается ли процесс current (процесс-получатель) каким-нибудь другим
процессом. В этом случае функция dosignaio вызывает функции
donotifyparentcidstopo и schedule о, чтобы сообщить отслеживающему
процессу об обработке сигнала.
Затем функция dosignaio записывает в локальную переменную ка адрес
структуры ksigaction сигнала, подлежащего обработке:
ka = ¤t->sig->action[signr-l];
В зависимости от содержимого структуры, возможен один из трех сценариев:
игнорирование сигнала, выполнение действия по умолчанию и вызов обра-
обработчика сигнала.
Когда добавленный сигнал игнорируется явно, функция dosignaio просто
переходит на новый шаг цикла и, следовательно, рассматривает следующий
висящий сигнал:
if (ka->sa.sa_handler == SIG_IGN)
continue;
В следующих двух разделах мы опишем, как выполняется действие по умол-
умолчанию, а также как выполняется обработчик сигнала.
Выполнение действия по умолчанию
по обработке сигнала
ЕСЛИ ПОЛе ka->sa.sa_handler содержит SIGJDFL, функция do_signal() ДОЛЖНа
выполнить действие по умолчанию для данного сигнала. Единственным ис-
исключением является ситуация, в которой принимающим процессом является
init. В этом случае сигнал просто отбрасывается, как описано в
разд. "Действия, выполняемые при доставке сигнала"ранее в этой главе:
if (current->pid == 1)
continue;
Для других процессов сигналы с действием по умолчанию "игнорировать"
обрабатываются достаточно легко:
if (signr==SIGCONT || signr==SIGCHLD ||
signr==SIGWINCH || signr==SIGURG)
continue;
Сигналы с действием по умолчанию "остановить" способны остановить все
процессы в группе. С этой целью функция dosignai о переводит процессы в
состояние taskstopped и затем вызывает функцию schedule () (см. главу 7):
if (signr==SIGSTOP || signr==SIGTSTP ||
signr==SIGTTIN || signr==SIGTTOU) {
if (signr != SIGSTOP &&
is_orphaned_pgrp(current->signal->pgrp))
continue;
do_signal_stop(signr);
}
Существует тонкое различие между сигналом sigstop и другими сигналами:
sigstop всегда останавливает выполнение группы потоков, а прочие сигналы
делают это, только когда группа не является "осиротевшей группой процес-
процессов". Стандарт POSIX гласит, что группа процессов не является "осиротев-
"осиротевшей", пока в ней есть процесс, имеющий родителя в другой группе процес-
процессов, но в том же сеансе. Таким образом, если процесс-родитель уничтожен,
но пользователь, запустивший его, не вышел из системы, группа процессов
не считается "осиротевшей".
Функция dosignaistopo проверяет, является ли процесс current первым
останавливаемым процессом в группе. Если это так, функция производит
"групповую остановку", а именно — записывает в поле groupstopcount де-
дескриптора сигнала положительное число и пробуждает все процессы в груп-
группе. Каждый такой процесс проверяет это поле и обнаруживает, что происхо-
происходит остановка группы. Тогда он изменяет свое состояние на taskstopped и
вызывает фуНКЦИЮ schedule (). Кроме ТОГО, функция dosignalstopO ПОСЫ-
лает сигнал sigchld процессу-родителю лидера группы, если этот родитель не
установил флаг sa_nocldstop для сигнала sigchld.
Сигналы, у которых действием по умолчанию является "Выполнить дамп",
могут создать файл core в рабочем каталоге процесса. Этот файл будет хра-
хранить все содержимое адресного пространства процесса и регистров процес-
процессора. После того как функция dosignai () создаст такой файл, она уничтожит
группу потоков. У остальных восемнадцати сигналов действием по умолча-
умолчанию является "Завершить выполнение", которое сводится к уничтожению
группы потоков. Чтобы уничтожить всю группу потоков, функция вызывает
функцию dogroupexit(), выполняющую корректный "групповой выход"
(см. главу 3).
Обработка сигнала
Если для сигнала был задан обработчик, функция dosignaio должна обес-
обеспечить его выполнение. С этой целью она вызывает функцию
handle_signal ()'.
handle_signal(signr, sinfo, &ka, oldset, regs);
if (ka->sa.sa_flags & SA_ONESHOT)
ka->sa.sa_handler = SIG_DFL;
return 1;
Если у принятого сигнала установлен флаг saoneshot, для обработки сигнала
должно быть возвращено действие по умолчанию, т. е. последующие отправ-
отправки этого сигнала не должны снова приводить к выполнению специального
обработчика. Обратите внимание, что функция dosignaio возвращает
управление после обработки одиночного сигнала. Другие висящие сигналы
не будут рассматриваться до следующего вызова функции dosignai (). Такой
подход гарантирует, что сигналы реального времени будут обработаны в
должном порядке.
Выполнение обработчика сигнала является довольно сложной задачей из-за
необходимости аккуратно "жонглировать" стеками при переключении с ре-
режима пользователя на режим ядра и обратно. Сейчас мы подробно разъясним,
что при этом происходит.
Обработчики сигналов — это функции, определяемые процессами режима
пользователя и включенные в сегмент кода режима пользователя. Функция
handiesignaio работает в режиме ядра, в то время как обработчики сигна-
сигналов работают в режиме пользователя. Это означает, что текущий процесс
должен вначале выполнить обработчик сигнала в режиме пользователя до
того, как ему будет разрешено продолжить свое "нормальное" выполнение.
Кроме того, когда ядро пытается продолжить нормальное выполнение про-
процесса, стек режима ядра уже не содержит аппаратный контекст прерванной
программы, потому что стек режима ядра очищается при каждом переходе из
режима пользователя в режим ядра.
Ситуация еще усложняется тем, что обработчики сигналов могут делать сис-
системные вызовы. В этом случае по окончании работы служебной процедуры
управление должно быть возвращено обработчику сигнала, а не коду пре-
прерванной программы.
Подход, принятый в Linux, заключается в копировании аппаратного контек-
контекста, хранящегося в стеке режима ядра, в стек режима пользователя текущего
процесса. Кроме того, стек режима пользователя модифицируется так, что по
окончании выполнения обработчика сигнала автоматически делается систем-
системный вызов sigreturno, копирующий аппаратный контекст в стек режима яд-
pa и восстанавливающий оригинальное содержимое стека режима пользова-
пользователя.
На рис. 11.2 показан ход выполнения функций, вовлеченных в обработку
сигнала. Процессу посылается незаблокированный сигнал. Когда возникает
прерывание или исключение, процесс переходит в режим ядра. Непосредст-
Непосредственно перед возвращением в режим пользователя ядро вызывает функцию
dosignaio, которая обрабатывает сигнал (вызывая функцию handie_
signal о) и заполняет стек режима пользователя (вызывая функцию
setupf rame () или setuprtf rame ()). Когда процесс снова переключается в
режим пользователя, он приступает к выполнению обработчика сигнала, по-
поскольку начальный адрес обработчика был принудительно записан в счетчик
команд. Когда функция-обработчик завершится, будет выполнен код, адрес
которого был помещен в стек режима пользователя функциями setupf rame ()
ИЛИ setuprtf rame (). Он делает системный ВЫЗОВ sigreturn() ИЛИ
rtsigretumo, а соответствующая служебная процедура копирует аппарат-
аппаратный контекст нормальной программы в стек режима ядра и восстанавливает
первоначальное состояние стека режима пользователя (вызывая функцию
restoresigcontext о). Когда системный вызов завершается, нормальная про-
программа может продолжить свое выполнение.
Теперь рассмотрим работу этой схемы более подробно.
Рис. 11.2. Обработка сигнала
Подготовка кадра
Чтобы корректно заполнить стек режима пользователя процесса, функция
handlesignalO вызывает либо функцию setupf rame () (ДЛЯ сигналов, КОТО-
рым не нужна таблица siginfot), либо функцию setup_rt_f rame () (для сиг-
сигналов, которым требуется таблица siginfot). Чтобы сделать выбор между
ЭТИМИ ДВуМЯ фуНКЦИЯМИ, ЯДрО Проверяет флаг SASIGINFO В ПОЛе sa_flags
таблицы sigaction, ассоциированной с сигналом.
Функция setupf rame () принимает четыре параметра, имеющие следующий
смысл:
□ sig — номер сигнала;
П ка — адрес таблицы ksigaction, ассоциированной с сигналом;
□ oidset — адрес массива, являющегося битовой маской заблокированных
сигналов;
□ regs — адрес области в стеке режима ядра, где хранится содержимое реги-
регистров режима пользователя.
Функция setupf rame () помещает в стек режима пользователя структуру,
называемую кадром, которая содержит информацию, необходимую для обра-
обработки сигнала и для корректного возврата в функцию sys_sigretum().
Кадр — это таблица типа sigframe, состоящая из следующих полей
(рис. 11.3):
□ pretcode — адрес возврата для обработчика сигнала. Это поле указывает
на КОД, ИМеЮЩИЙ метку kernel_sigreturn;
□ sig — номер сигнала. Это параметр, необходимый обработчику сигнала;
П sc— структура типа sigcontext, содержащая аппаратный контекст про-
процесса в режиме пользователя непосредственно перед переходом в режим
ядра (эта информация копируется из стека режима ядра процесса current).
Структура содержит также битовый массив, задающий заблокированные
обычные сигналы процесса;
Рис. 11.3. Кадр в стеке режима пользователя
□ fpstate — структура типа fpstate, которую можно использовать для хра-
хранения регистров операций с плавающей точкой, принадлежащих процессу
в режиме пользователя (см. главу 3);
□ extramask— битовый массив, задающий заблокированные сигналы реаль-
реального времени;
□ retcode— 8-баЙТОВЫЙ КОД, ВЫПОЛНЯЮЩИЙ Системный ВЫЗОВ sigreturn().
В прежних версиях Linux этот код использовался для возврата из обработ-
обработчика сигнала, но в Linux 2.6 он применяется только в качестве сигнатуры,
позволяющей отладчикам распознать кадр стека сигнала.
ФунКЦИЯ setupf rame () Начинается С ВЫЗОВа функции getsigf rame (), ВЫЧИС-
ляющей первую ячейку кадра. Эта ячейка памяти обычно6 принадлежит стеку
режима пользователя, поэтому функция возвращает значение:
(regs->esp — sizeof(struct sigframe)) & 0xfffffff8
Поскольку стеки растут в направлении нижних адресов, начальный адрес
кадра получается вычитанием его размера из адреса вершины стека и вырав-
выравниванием результата по числу, кратному 8.
Затем возвращенный адрес проверяется макросом accessok. Если адрес яв-
является ДОПУСТИМЫМ, фуНКЦИЯ МНОГОКраТНО ВЫЗЫВаеТ фуНКЦИЮ put_user()
для заполнения всех полей кадра. Поле pretcode инициализируется значением
& kernei_sigreturn, являющимся адресом некоторого "склеивающего" кода,
помещенного в страницу vsyscall (см. главу 10).
Сделав это, функция модифицирует область regs в стеке режима пользовате-
пользователя, обеспечивая тем самым передачу управления обработчику сигнала, когда
процесс current возобновит свое выполнение в режиме пользователя:
regs->esp = (unsigned long) frame;
regs->eip = (unsigned long) ka->sa.sa_handler;
regs->eax = (unsigned long) sig;
regs->edx = regs->ecx = 0;
regs->xds = regs->xes = regs->xss = USER_DS;
regs->xcs = USER_CS;
Функция setupframeo завершает работу, сбрасывая регистры сегментации,
сохраненные в стеке режима ядра, в значения по умолчанию. Теперь инфор-
6 Linux разрешает процессам задавать альтернативный стек для их обработчиков сигналов с по-
помощью системного вызова signaltstack (). Кроме того, подобная возможность является требова-
требованием стандарта Х/Open. При наличии альтернативного стека функция get_sigf rame () возвращает
адрес внутри него. Мы больше не возвратимся к этой теме, поскольку концептуально здесь все ана-
аналогично обычной обработке сигнала.
мация, необходимая обработчику сигнала, находится наверху стека режима
пользователя.
Функция setuprtf rame () аналогична фуНКЦИИ setupf rame (), НО Она ПОМе-
щает в стек режима пользователя расширенный кадр (хранящийся в структу-
структуре rtsigframe), КОТОрЫЙ ВКЛЮЧаеТ В Себя И СОДерЖИМОе таблицы siginfot,
ассоциированной с сигналом. Кроме того, данная функция заполняет поле
pretcode так, чтобы оно указывало на код kerneirtsigreturn на странице
vsyscall.
Проверка флагов сигнала
Заполнив стек режима пользователя, функция handiesignai о проверяет зна-
значения флагов, ассоциированных с сигналом. Если у сигнала не установлен
флаг SANODEFER, сигналы В ПОЛе samask таблицы sigaction ДОЛЖНЫ быть
блокированы на время выполнения обработчика сигнала:
if (!(ka->sa.sa_flags & SA_NODEFER)) {
spin_lock_irq(¤t->sighand->siglock);
sigorsets(¤t->blocked, ¤t->blocked, &ka->sa.sa_mask);
sigaddset(¤t->blocked, sig);
recalc_sigpending(current);
spin_unlock_irq(¤t->sighand->siglock);
}
Как было отмечено ранее, функция recaicsigpendingo проверяет, есть ли у
процесса незаблокированные висящие сигналы, и соответствующим образом
устанавливает флаг tif_sigpending.
Затем функция возвращает управление функции do signal о, которая тоже
немедленно возвращает управление.
Запуск обработчика сигналов
Когда функция dosignaio возвращает управление, текущий процесс возоб-
возобновляет свое выполнение в режиме пользователя. Благодаря подготовитель-
подготовительной работе, проведенной функцией setupf rame (), регистр eip указывает на
первую инструкцию обработчика сигнала, а регистр esp — на первую ячейку
кадра, расположенного на верхушке стека режима пользователя. В результате
выполняется обработчик сигнала.
Завершение обработчика сигнала
Когда обработчик сигнала завершает выполнение, адрес возврата на верхуш-
верхушке стека указывает на код на странице vsyscall, на который ссылается поле
pretcode кадра:
kernel_sigreturn:
popl %eax
movl $ NR_sigreturn, %eax
int $0x80
Таким образом, номер сигнала (то есть поле sig кадра) выбрасывается из сте-
стека. Затем выполняется системный вызов sigreturn ().
Функция syssigreturno вычисляет адрес структуры regs, имеющей тип
ptregs, которая содержит аппаратный контекст процесса в режиме поль-
пользователя (см. главу 10). По значению, хранящемуся в поле esp, функция
может вычислить и проверить адрес кадра внутри стека режима пользова-
пользователя:
frame = (struct sigframe *)(regs.esp — 8);
if (verify_area(VERIFY_READ, frame, sizeof(*frame)) {
force_sig(SIGSEGV, current);
return 0;
}
Затем функция копирует битовый массив (определяющий сигналы, заблоки-
заблокированные до вызова обработчика сигналов) из поля sc кадра в поле blocked
процесса current. В результате все сигналы, замаскированные перед выпол-
выполнением обработчика сигналов, перестают быть заблокированными. После
ЭТОГО Вызывается фуНКЦИЯ recalc_sigpending ().
Функция syssigreturno должна в этом месте скопировать аппаратный кон-
контекст процесса из поля sc кадра в стек режима ядра и удалить кадр из стека
режима пользователя. Она выполняет эти действия с помощью функции
restore_sigcontext().
ЕСЛИ СИГНаЛ был ПОСЛаН СИСТеМНЫМ ВЫЗОВОМ, Например, rt_sigqueueinfo(),
которому требуется, чтобы с сигналом была ассоциирована таблица
siginfot, принцип работы тот же самый. Поле pretcode расширенного кадра
указывает на код kerneirtsigretum на странице vsyscall, который делает
системный вызов rt_sigreturn(). Соответствующая служебная процедура
sysrtsigretumO копирует аппаратный контекст процесса из расширенного
кадра в стек режима ядра и восстанавливает оригинальное содержание стека
режима пользователя, удаляя из него расширенный кадр.
Повторное выполнение системных вызовов
Запрос, связанный с системным вызовом, не всегда может быть немедленно
удовлетворен ядром. В таких случаях процесс, сделавший системный вызов,
переводится в состояние task_interruptible или task_uninterruptible.
Если процесс переведен в состояние taskjenterruptible, и какой-то другой
процесс посылает ему сигнал, ядро переводит первый процесс в состояние
taskrunning, не завершив выполнение системного вызова (см. главу 4). Сиг-
Сигнал доставляется процессу при переключении в режим пользователя. Когда
это происходит, служебная процедура системного вызова не завершает свою
работу, а возвращает код ошибки eintr, erestartnohand, erestart_
RESTARTBLOCK, ERESTARTSYS ИЛИ ERESTARTNOINTR.
На практике единственным кодом ошибки, который может быть получен
процессом режима пользователя в такой ситуации, является eintr, и это оз-
означает, что системный вызов не был завершен. (Программист может прове-
проверить этот код и решить, следует ли повторить системный вызов.) Остальные
коды ошибок предназначены для внутреннего пользования, и ядро применяет
их для уточнения, может ли системный вызов быть выполнен повторно и ав-
автоматически по окончании обработчика сигнала.
В табл. 11.11 перечислены коды ошибок, имеющие отношение к незавершен-
незавершенным системным вызовам, и указано их влияние на каждое из трех действий
по обработке сигналов. В таблице употребляются следующие термины:
□ Завершить выполнение — системный вызов не будет повторен автомати-
автоматически. Процесс возобновит работу в режиме пользователя с инструкции,
следующей за инструкцией int $0x8о или sysenter, а регистр еах будет со-
содержать значение -eintr;
□ Выполнить повторно — ядро заставляет процесс режима пользователя за-
заново загрузить в регистр номер системного вызова и выполнить инструк-
инструкцию int $0x80 или sysenter. Процессу не известно, что выполнение явля-
является повторным, и код ошибки ему не передается;
□ По обстоятельствам— системный вызов выполняется повторно, только
если у доставленного сигнала установлен флаг sarestart. В противном
случае системный вызов завершается с кодом ошибки -eintr.
Таблица 11.11. Повторное выполнение системных вызовов
Коды ошибок и их влияние на выполнение системного вызова
п й I I I ERESTARTNOHM I
действие E|NJR ERESTARTSYS ERESTART_ ERESTARTNOINTR
сигнала RESTARTBLOCK
По умол- Завершить Выполнить Выполнить повторно Выполнить
чанию выполнение повторно повторно
Игнориро- Завершить Выполнить Выполнить повторно Выполнить
вать выполнение повторно повторно
Обрабо- Завершить По обстоя- Завершить выполнение Выполнить
тать выполнение тельствам повторно
При доставке сигнала ядро должно быть уверено, что процесс действительно
сделал системный вызов прежде, чем оно попытается выполнить его повтор-
повторно. Здесь важнейшую роль играет поле origeax аппаратного контекста regs.
Вспомним, как инициализируется это поле, когда запускается обработчик
прерывания или исключения.
□ Прерывание — поле содержит номер IRQ, ассоциированный с прерывани-
прерыванием, минус 256 (см. главу 4).
П Исключение 0x80 (а также sysenter) — поле содержит номер системного
вызова (см. главу 10).
П Прочие исключения — поле содержит значение -1 (см. главу 4),
Таким образом, неотрицательное значение в поле origeax свидетельствует о
том, что сигнал разбудил процесс, который находится в состоянии
taskinterruptible, и ждал завершения системного вызова. Служебная про-
процедура распознает, что системный вызов был прерван, и возвращает один из
вышеупомянутых кодов ошибки.
Повторный запуск системного вызова,
прерванного необработанным сигналом
Если сигнал явно игнорируется процессом, или выполняется его действие по
умолчанию, функция dosignaio анализирует код ошибки системного вызо-
вызова, чтобы, в соответствии с табл. 11.11, принять решение об автоматическом
повторении неоконченного системного вызова. Если вызов должен быть по-
повторен, функция модифицирует аппаратный контекст regs так, чтобы при
возвращении процесса в режим пользователя регистр eip указывал либо на
инструкцию int $0x80, либо на sysenter, а регистр еах содержал номер сис-
системного вызова:
if (regs->orig_eax >= 0) {
if (regs->eax == -ERESTARTNOHAND || regs->eax = -ERESTARTSYS ||
regs->eax == -ERESTARTNOINTR) {
regs->eax = regs->orig_eax;
regs->eip -= 2;
}
if (regs->eax == -ERESTART_RESTARTBLOCK) {
regs->eax = NR_restart_syscall;
regs->eip -= 2;
}
}
В поле regs->eax записывается код возврата служебной процедуры системно-
системного вызова (см. главу 10). Обратите внимание, что обе инструкции, int $0x8 о и
sysreturn, имеют длину два байта, и поэтому функция вычитает 2 из eip, что-
чтобы там был адрес инструкции, производящей системный вызов.
Код ошибки erestartrestartblock является специальным, потому что в ре-
регистр еах записывается номер системного вызова restartsyscaii о, и, следо-
следовательно, процесс в режиме пользователя не повторяет именно тот систем-
системный вызов, который был прерван сигналом. Данный код ошибки использует-
используется только системными вызовами, работающими с показаниями часов. При
повторном запуске такие системные вызовы должны подстраивать свои па-
параметры режима пользователя. Типичным примером является системный
вызов nanosieepo (см. главу 6). Предположим, процесс делает этот вызов,
чтобы приостановить выполнение на 20 мс, а через 10 мс приходит сигнал.
Если бы системный вызов был повторен обычным образом, общее время за-
задержки составило бы 30 мс.
Вместо этого служебная процедура системного вызова nanosieepo записыва-
записывает в поле restartbiock структуры threadinfo процесса current адрес специ-
специальной служебной процедуры, которая будет вызвана при рестарте, и воз-
возвращает -erestartrestartblock, когда ее работы прерываются. Служебная
процедура sysrestartsyscaii о всего лишь выполняет служебную процеду-
процедуру специального вызова nanosieepo, которая регулирует задержку с учетом
времени, прошедшего между запуском первоначального системного вызова и
его рестартом.
Повторный запуск системного вызова
для обработанного сигнала
Если сигнал обрабатывается, функция handiesignaio анализирует код
ошибки и, возможно, флаг sarestart в таблице sigaction, чтобы принять
решение об автоматическом повторении неоконченного системного вызова:
if (regs->orig_eax >= 0) {
switch (regs->eax) {
case -ERESTART_RESTARTBLOCK:
case -ERESTARTNOHAND:
regs->eax = -EINTR;
break;
case -ERESTARTSYS:
if (!(ka->sa.sa_flags & SA_RESTART)) {
regs->eax = -EINTR;
break;
}
/* fallthrough */
case -ERESTARTNOINTR:
regs->eax = regs->orig_eax;
regs->eip -= 2;
}
}
Если вызов должен быть повторен, функция handiesignai () действует в точ-
точности, как функция dosignai о; в противном случае она возвращает процес-
процессу режима пользователя код ошибки -eintr.
Системные вызовы,
связанные с обработкой сигналов
Как было сказано во введении к этой главе, программам, работающим в ре-
режиме пользователя, разрешено посылать и принимать сигналы. Отсюда сле-
следует необходимость определения некоторого набора системных вызовов,
обеспечивающих выполнение подобных операций. К сожалению, в силу ис-
исторических причин существует несколько системных вызовов, служащих
фактически той же цели. В результате некоторые из них остаются невостре-
невостребованными. Например, Системные ВЫЗОВЫ sys_sigaction() И sys_rt_
sigactiono практически идентичны, и поэтому интерфейсная функция
sigactionO, включенная в библиотеку С, в конечном счете, вызывает
sys_rt_sigaction(), а не sys_sigaction(). В следующих разделах мы опишем
некоторые из наиболее важных системных вызовов.
Системный вызов kill()
Системный вызов kill (pid, sig) широко применяется для отправки сигналов
обычным процессам или многопоточным приложениям. Его служебной про-
процедурой является функция syskiiio. Целочисленный параметр pid имеет
разный смысл, в зависимости от своего числового значения:
□ pid > 0 — сигнал sig посылается группе потоков, содержащей процесс,
у которого идентификатор PID равен pid;
□ pid = 0 — сигнал sig посылается всем группам потоков в той же группе
процессов, что и вызывающий процесс;
□ pid = -1 — сигнал посылается всем процессам, кроме (PID 0), init (PID 1)
И current;
□ pid < -1 — сигнал посылается всем группам потоков в группе процессов
-pid.
Функция syskiiio заполняет минимальную таблицу siginfot для сигнала
И Затем ВЫЗЫВает функцию kill_something_infо ():
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SIJJSER;
info._sifields._kill._pid = current->tgid;
info, sifields. kill, uid = current->uid;
return kill_something_info(sig, &info, pid);
Функция kilisomethinginfoO, в свою очередь, вызывает либо функцию
kiliprocinfoo (чтобы послать сигнал одной группе потоков при помощи
ФУНКЦИИ group_send_sig_info())? либо функцию kill pg info () (чтобы пере-
брать все процессы в целевой группе процессов и вызвать функцию
sendsiginfoO ДЛЯ каЖДОГО ИЗ НИХ), либо функцию group_send_sig_info(),
причем последняя вызывается для каждого процесса в системе (если пара-
параметр pid равен -1).
Системный вызов kill о способен послать любой сигнал, даже так называе-
называемые сигналы реального времени, номера которых лежат в диапазоне от 32
до 64. Однако, как мы видели в разд. "Генерирование сигнала" ранее в этой
главе, системный вызов kill о не гарантирует добавление нового элемента в
очередь висящих сигналов процесса-получателя, так что повторные экземп-
экземпляры висящих сигналов могут быть потеряны. Сигналы реального вре-
времени следует посылать, например, с помощью системного вызова
rt_sigqueueinfo() (см. разд. "Системные вызовы для сигналов реального вре-
времени" далее в этой главе).
В таких Unix-подобных системах, как System V и BSD, имеется дополнитель-
дополнительный системный вызов kilipgo, который может явным образом послать сиг-
сигнал группе процессов. В Linux эта возможность реализована в виде библио-
библиотечной функции, делающей системный вызов kill о. Еще одной возможно-
возможностью является функция raise (), которая посылает сигнал текущему процессу
(то есть процессу, который ее выполняет). В Linux она реализована в виде
библиотечной функции.
Системные вызовы tkillQ и tgkill()
Системные вызовы tkiiio и tgkiiio посылают сигналы конкретному про-
процессу в группе. Функция pthreadkiii о каждой библиотеки pthread, удовле-
удовлетворяющей стандарту POSIX, делает один из этих вызовов для отправки сиг-
сигнала заданному облегченному процессу.
Системный вызов tkiiio принимает два параметра: pid— идентификатор
процесса, которому посылается сигнал, и sig — номер сигнала. Служебная
процедура systkiiio заполняет таблицы siginfo, вычисляет адрес дескрип-
дескриптора процесса, делает некоторые проверки (вроде тех, что описаны в шаге 2
в разд. "Функция group send siginfо ()"ранее в этой главе) и вызывает функ-
функцию specif icsendsiginf о (), чтобы ПОСЛать Сигнал.
Системный вызов tgkiiio отличается от tkiiio, поскольку имеет третий
параметр, tgid — идентификатор группы потоков, в которую входит процесс-
получатель сигнала. Служебная процедура systgkiiio выполняет те же
действия, что и sys_tkiii(), но, кроме того, проверяет принадлежность про-
процесса-получателя к группе tgid. Эта дополнительная проверка решает про-
проблему одновременного доступа, которая возникает, когда уничтожаемому
процессу посылается какой-то сигнал: если другое многопоточное приложе-
приложение создает облегченные процессы достаточно быстро, сигнал может быть
доставлен не тому процессу. Системный вызов tgkiii о справляется с ситуа-
ситуацией, потому что идентификатор группы потоков не меняется на протяжении
существования многопоточного приложения.
Изменение действия сигнала
Системный вызов sigaction(sig,act,oact) позволяет пользователям опреде-
лять действие сигнала. Естественно, если это не сделано, ядро выполняет
действие по умолчанию, ассоциированное с доставленным сигналом.
Служебная процедура syssigactiono принимает два параметра: sig— но-
номер сигнала и act— таблицу типа oidsigaction, которая задает новое дей-
действие.
Третий, необязательный выходной параметр oact может быть использован
для получения информации о предыдущем действии, ассоциированном с сиг-
сигналом. Структура oidsigaction состоит из тех же полей, что и структура
sigaction, описанная ранее, но поля расположены в другом порядке.
Функция проверяет допустимость адреса act. Затем она заполняет поля
sahandler, saflags И samask локальной переменной newka, имеющей ТИП
ksigaction, значениями из соответствующих полей таблицы *act:
get_user(new_ka.sa.sa_handler, &act->sa_handler);
get_user(new_ka.sa.sa_flags, &act->sa_flags);
get_user(mask, &act->sa_mask);
siginitset(&new_ka.sa.sa_mask, mask);
Функция вызывает функцию dosigactiono, чтобы скопировать таблицу
newka в элемент с индексом sig-i в массиве current->sig->action (номер
сигнала на единицу больше индекса элемента, поскольку нет сигнала с нуле-
нулевым номером):
k = ¤t->sig->action[sig-l];
if (act) {
*к = *act;
sigdelsetmask(&k->sa.sa_mask, sigmask(SIGKILL) | sigmask(SIGSTOP));
if (k->sa.sa_handler == SIG_IGN || (k->sa.sa_handler = SIG_DFL &&
(sig=SIGCONT || sig==SIGCHLD || sig==SIGWINCH || sig==SIGURG) ) ) {
rm_from_queue (sigmask (sig), ¤t->signal->shared_pending);
t = current;
do {
rm_from_queue(sigmask(sig), ¤t->pending);
recalc_sigpending_tsk(t);
t = next_thread(t);
} while (t != current);
}
}
Стандарт POSIX требует, чтобы установка действия сигнала в значение
sig_ign или sigdfl, когда действием по умолчанию является "игнорировать",
приводила к отбрасыванию каждого висящего сигнала того же типа. Следует
также обратить внимание, что, независимо от того, какие сигналы пытается
замаскировать пользователь для обработчика сигнала, сигналы sigkill и
sigstop никогда не маскируются.
Системный вызов sigactiono, кроме прочего, позволяет пользователю ини-
инициализировать поле safiags в таблице sigaction. Допустимые значения это-
этого поля и их смысл даны в табл. 11.6.
Старые варианты System V предлагают системный вызов signal (), до сих пор
широко используемый программистами. В новых версиях библиотек С
signal о реализован с помощью функции rtsigactiono. Однако Linux до
сих пор поддерживает старые библиотеки С и предлагает служебную про-
процедуру sys_signal():
new_sa.sa.sa_handler = handler;
new_sa.sa.sa_flags = SA_ONESHOT | SAJTOMASK;
ret = do_sigaction(sig, &new_sa, &old_sa);
return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
Просмотр висящих заблокированных сигналов
Системный вызов sigpendingo позволяет процессу просмотреть набор вися-
висящих заблокированных сигналов, т. е. сигналов, которые были посланы, буду-
будучи заблокированными. Соответствующая служебная процедура sys_
sigpendingo принимает единственный параметр, set, содержащий адрес
пользовательской переменной, в которую следует скопировать битовый мас-
массив:
sigorsets(Spending, ¤t->pending.signal,
¤t->signal->shared_pending.signal);
sigandsets(Spending, ¤t->blocked, Spending);
copy_to_user(set, Spending, 4);
Модификация набора
заблокированных сигналов
Системный вызов sigprocmasko позволяет процессам изменять набор
заблокированных сигналов. Он действует только на обычные сигналы (не
являющиеся сигналами реального времени). Служебная процедура sys_
sigprocmask () принимает три параметра:
□ oset — указатель в адресном пространстве процесса на битовый массив, в
котором следует сохранить предыдущую битовую маску;
□ set — указатель в адресном пространстве процесса на битовый массив,
содержащий новую битовую маску;
□ how — флаг, принимающий одно из следующих значений:
• sigblock— массив-маска *set задает сигналы, которые должны быть
добавлены к массиву-маске заблокированных сигналов;
• sigunblock— массив-маска *set задает сигналы, которые должны
быть удалены из массива-маски заблокированных сигналов;
• sigsetmask— массив-маска *set задает новую битовую маску забло-
заблокированных сигналов.
Функция вызывает функцию copyfromusero, чтобы скопировать значение,
на которое указывает параметр set, в локальную переменную newset, а так-
также копирует массив-маску стандартных заблокированных сигналов процесса
current в локальную переменную oidset. Затем она выполняет над этими
двумя переменными действие, определяемые флагом how:
if (copy_from_user(&new_set, set, sizeof (*set)))
return -EFAULT;
new_set &= ~(sigmask(SIGKILL)|sigmask(SIGSTOP));
old_set = current->blocked.sig[0];
if (how == SIG_BLOCK)
sigaddsetmask(¤t->blocked, new_set);
else if (how == SIGJJNBLOCK)
sigdelsetniask(¤t->blocked, new_set);
else if (how == SIG_SETMASK)
current->blocked.sig[0] = new_set;
else
return -EINVAL;
recalc sigpending(current);
if (oset && copy_to_user(oset, &old_set, sizeof(*oset)))
return -EFAULT;
return 0;
Приостановка выполнения процесса
Системный вызов sigsuspendo переводит процесс в состояние task_
interruptible, предварительно заблокировав стандартные сигналы, заданные
массивом-маской, на которую указывает параметр mask. Процесс будет раз-
разбужен только не игнорируемым, незаблокированным сигналом.
Служебная процедура syssigsuspend () выполняет следующий код:
mask &= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));
saveset = current->blocked;
siginitset(¤t->blocked, inask);
recalc_sigpending(current);
regs->eax = -EINTR;
while A) {
current->state = TASK_INTERRUPTIBLE;
schedule();
if (do_signal(regs, &saveset))
return -EINTR;
}
Функция schedule () выбирает, какой процесс будет выполняться дальше. Ко-
Когда процесс, сделавший системный вызов sigsuspendo, выполняется снова,
функция syssigsuspendO вызывает функцию do_signal() ДЛЯ ДОСТавКИ СИГ-
нала, который разбудил процесс. Если последняя функция возвратит едини-
единицу, сигнал не был проигнорирован. Таким образом, системный вызов завер-
завершается возвратом кода ошибки -eintr.
Системный вызов sigsuspendo может показаться избыточным, поскольку
комбинация вызовов sigprocmasko и sleep о предположительно приведет к
тому же результату. На самом деле это не так. Процессы могут чередоваться
в любой момент, и необходимо отдавать себе отчет, что если вы сделаете
системный вызов, выполняющий действие А, а затем системный вызов, вы-
выполняющий действие В, то это не будет эквивалентно одному системному
вызову, выполняющему сначала действие А, а потом — действие В.
В этом конкретном случае системный вызов sigprocmasko может отменить
блокирование сигнала, который будет доставлен до выполнения системного
вызова sleep о. Если такое произойдет, процесс может навсегда остаться в
состоянии taskinterruptible, ожидая сигнал, который уже доставлен. Зато
системный вызов sigsuspendo не разрешает отправку сигналов после отмены
их блокирования и до вызова функции schedule о, потому что другим про-
процессам нельзя захватывать процессор в течение этого промежутка времени.
Системные вызовы
для сигналов реального времени
Поскольку системные вызовы, рассмотренные в предыдущих разделах, име-
имеют отношение только к стандартным сигналам, возникает необходимость в
дополнительных системных вызовах, которые позволили бы процессам ре-
режима пользователя обрабатывать сигналы реального времени.
Некоторые системные вызовы для сигналов реального времени
(rt_sigaction(), rt_sigpending (), rt_sigprocmask() И rt_sigsuspend() ) анало-
аналогичны вызовам, описанным ранее, и мы не будем их обсуждать. По этой же
причине мы не станем обсуждать два других системных вызова, работающих
с очередями сигналов реального времени:
□ rtsigqueueinfoo — посылает сигнал реального времени, так что он до-
добавляется в очередь совместно используемых висящих сигналов процесса-
получателя. Обычно этот вызов делается с помощью стандартной библио-
библиотечной функции sigqueue ();
□ rtsigtimedwait о — выводит из очереди заблокированный висящий сиг-
сигнал без его доставки и возвращает вызвавшей функции номер этого сигна-
сигнала. Если висящие заблокированные сигналы отсутствуют, приостанав-
приостанавливает текущий процесс на фиксированный интервал времени. Обычно
этот вызов делается с помощью стандартных библиотечных функций
sigwaitinfо() И sigtimedwait().
ГЛАВА 12
Виртуальная файловая система
Одной из причин успешности системы Linux является ее способность к ком-
комфортному сосуществованию с другими системами. Вы можете без труда мон-
монтировать диски или разделы с форматом, используемым в Windows, других
системах семейства Unix или даже системах, занимающих незначительные
сектора рынка, таких как Amiga. Как и другие Unix-подобные системы, Linux
может поддерживать несколько типов файловых систем с помощью того, что
называется виртуальной файловой системой.
Идея, лежащая в основе виртуальной файловой системы, заключается в том,
чтобы занести в ядро большой объем информации для представления различ-
различных типов файловых систем. Существует поле или функция для поддержки
каждой операции всех реальных файловых систем, поддерживаемых в Linux.
Всякий раз, когда вызывается функция чтения, записи или какая-то другая,
ядро подставляет вместо нее функцию из "родной" файловой системы NTFS
или иной системы, которой принадлежит данный файл.
В этой главе обсуждаются задачи, структура и реализация виртуальной фай-
файловой системы Linux. Особое внимание уделено трем из пяти стандартных
типов файлов в Unix, а именно обычным файлам, каталогам и символьным
ссылкам. Файлы устройств описаны в главе 13, а каналы — в главе 19. Для
демонстрации работы реальной файловой системы в главе 18 описывается
Second Extended Filesystem, имеющаяся почти во всех Linux-системах.
Роль виртуальной файловой системы (VFS)
Виртуальная система, также известная под названием виртуальный пере-
переключатель файловых систем (Virtual Filesystem Switch, VFS), является про-
программным слоем ядра, который обрабатывает все системные вызовы, имею-
щие отношение к стандартной файловой системе Unix. Его основное досто-
достоинство состоит в предоставлении общего интерфейса к нескольким видам
файловых систем.
Представим, например, что пользователь вводит следующую команду:
$ср /floppy/TEST /tmp/test
где /floppy— точка монтирования дискеты MS-DOS, a /trap— обычный ка-
каталог файловой системы Second Extended Filesystem (Ext2). Слой VFS являет-
является абстрактным слоем между прикладной программой и реализациями фай-
файловых систем (рис. 12.1, а). Следовательно, программа ср не обязана знать
типы файловых систем файлов /floppy/TEST и /tmp/test. Она просто взаимо-
взаимодействует с VFS при помощи системных вызовов Unix, известных каждому,
кто программировал в этой ОС (см. главу 1). Код, выполняемый программой
ср, показан на рис. 12.1, б.
Рис. 12.1. Роль VFS в простой операции копирования файлов
Файловые системы, поддерживаемые VFS, можно разбить на три основных
класса:
□ Дисковые файловые системы — эти файловые системы управляют про-
пространством на локальном диске или памятью в каком-либо устройстве,
эмулирующем его (например, USB флэш-накопителе). Вот некоторые из
хорошо известных дисковых файловых систем, поддерживаемых VFS:
• файловые системы для Linux, такие как широко распространенная
Second Extended Filesystem (Ext2), недавно появившаяся Third Extended
Filesystem (Ext3), а также Reiser Filesystem (Reiser FSI;
1 Хотя эти файловые системы обязаны своим рождением Linux, они были перенесены на некоторые
другие операционные системы.
• файловые системы для различных вариантов Unix, такие как sysv
(System V, Coherent, Xenix), UFS (BSD, Solaris, NEXTSTEP) файловая
система MINIX, а также VERITAS VxFS (SCO UnixWare);
• файловые системы Microsoft, такие как MS-DOS, VFAT (Windows 95 и
последующие версии) и NTFS (Windows NT 4 и последующие версии);
• файловая система ISO 9660 (бывшая High Sierra Filesystem) для ком-
компакт-диска и файловая система Universal Disk Format (UDF) для DVD;
• прочие проприетарные файловые системы, например, OS/2 (HPFS) от
IBM, Apple Macintosh (HFS), Amiga Fast Filesystem (AFFS) и Acorn Disk
Filing System (ADFS);
• дополнительные журналируемые файловые системы, ведущие свое
происхождение от систем, отличных от Linux, например, JFS фирмы
IBM и XFS фирмы SGI.
П1 Сетевые файловые системы — эти файловые системы обеспечивают про-
простой доступ к файлам на других компьютерах, объединенных в сеть. При-
Примерами известных сетевых файловых систем, поддерживаемых VFS, яв-
являются NFS, Coda, AFS (Andrew filesystem), CIFS (Common Internet File
System, Общая файловая система Интернета), используемая в Microsoft
Windows, и NCP (NetWare Core Protocol) от фирмы Novell.
П Специальные файловые системы — эти файловые системы не управляют
пространством на диске ни локально, ни удаленно. Типичным примером
специальной файловой системы является /ргос (см. разд. "Специальные
файловые системы1' далее в этой главе).
В этой книге подробно описываются только файловые системы Ext2 и Ext3
(см. главу 18); прочие файловые системы не рассматриваются из-за недостат-
недостатка места.
Как было сказано в разд. "Обзор файловой системы ОС Unix" главы 7, ката-
каталоги Unix образуют дерево с корневым каталогом /. Корневой каталог нахо-
находится в корневой файловой системе, которой в Linux обычно является Ext2
или Ext3. Все остальные файловые системы могут быть "смонтированы" в
подкаталогах корневой файловой системы.
( Примечание )
Когда файловая система смонтирована в каталоге, содержимое каталога в ро-
родительской файловой системе более не доступно, поскольку каждый путь,
включая точку монтирования, будет ссылаться на смонтированную файловую
систему. Однако после размонтирования файловой системы содержимое ори-
оригинального каталога снова становится доступным. Эта несколько неожиданная
особенность файловых систем Unix используется системными администрато-
администраторами для сокрытия файлов: они просто монтируют файловую систему на ката-
каталог, содержащий файлы, которые необходимо скрыть.
Дисковая файловая система обычно хранится на реальном блочном устройст-
устройстве, например, на жестком диске, дискете или компакт-диске. Полезной осо-
особенностью VFS в Linux является возможность работы с виртуальными блоч-
блочными устройствами, например, /dev/loopO. Этим можно воспользоваться для
монтирования файловых систем, хранящихся в обычных файлах. Например,
пользователь может защитить свою личную файловую систему, сохранив ее
зашифрованную версию в обычном файле.
Первая виртуальная файловая система появилась в SunOS фирмы Sun
Microsystems в 1986 г. С тех пор большинство файловых систем Unix вклю-
включает в себя VFS. Однако VFS операционной системы Linux поддерживает
самый широкий диапазон файловых систем.
Общая файловая модель
Основная идея, лежащая в основе VFS, сводится к созданию общей файловой
модели, способной представить все поддерживаемые файловые системы. Эта
модель строго отражает файловую модель традиционной файловой системы
Unix, что неудивительно, поскольку Linux стремится работать со своей "род-
"родной" файловой системой с минимальными накладными расходами. Однако
реализация каждой конкретной файловой системы должна отображать свою
физическую организацию в общую файловую модель VFS.
Например, в общей файловой модели каждый каталог считается файлом, ко-
который содержит список файлов и других каталогов. Однако некоторые фай-
файловые системы в ОС, отличных от Unix, используют таблицу размещения
файлов (FAT), в которой хранится адрес каждого файла из дерева каталогов.
В таких файловых системах каталоги не являются файлами. Чтобы удовле-
удовлетворять требованиям общей файловой модели VFS, реализация файловой
системы, работающей с FAT, в операционной системе Linux должна быть в
состоянии динамически создавать файлы, соответствующие каталогам, когда
потребуется. Такие файлы существуют только как объекты в памяти ядра.
Что более важно, в ядре Linux нельзя жестко закодировать какую-то конкрет-
конкретную функцию для обработки операции, например, read () или iocti (). Вместо
этого приходится использовать указатель, которому присваивается адрес со-
соответствующей функции в конкретной файловой системе.
Чтобы проиллюстрировать этот подход, покажем, как функция read (), вызы-
вызываемая в коде на рис. 12.1, может быть транслирована ядром в вызов, специ-
специфичный для файловой системы MS-DOS. Когда приложение вызывает read (),
ядро обращается к служебной процедуре sysreado, как сделал бы любой
другой системный вызов. Как мы увидим далее в этой главе, файл представ-
представлен структурой file в памяти ядра. Эта структура имеет поле fop, содержа-
щее указатели на функции, специфичные файловой системы MS-DOS, в том
числе и на функцию чтения файла. Процедура sysreado находит указатель
на эту функцию и вызывает ее. Таким образом, вызов read о превращается
в непрямой вызов:
file->f_op->read(...)
Аналогичным образом, функция write () обеспечивает выполнение функции
записи из файловой системы Ext2, которая пишет в выходной файл. Короче
говоря, ядро ответственно за присваивание соответствующего набора указа-
указателей переменной file, ассоциированной с каждым открытым файлом, а за-
затем — за вызов функции, специфичной для каждой файловой системы, на
которую указывает поле fop.
Можно считать общую файловую модель объектно-ориентированной. Тогда
объектом будет программная конструкция, определяющая как структуру дан-
данных, так и методы, работающие с ней. Из соображений эффективности, сис-
система Linux не кодируется на объектно-ориентированном языке, таком как
C++. Объекты реализованы в виде простых структур данных языка С, причем
некоторые их поля указывают на функции, соответствующие методам этого
объекта.
Общая файловая модель включает в себя следующие типы объектов:
□ суперблок — этот объект хранит информацию, относящуюся к смонтиро-
смонтированной файловой системе. В случае дисковых операционных систем он
обычно соответствует управляющему блоку файловой системы, храняще-
хранящемуся на диске;
□ индексный дескриптор — этот объект хранит в себе общую информацию о
конкретном файле. В случае дисковых операционных систем он обычно
соответствует управляющему блоку файла, хранящемуся на диске. Каж-
Каждый индексный дескриптор связан со специальным номером, уникально
идентифицирующим файл в файловой системе;
□ файловый объект— этот объект хранит информацию о взаимодействии
процесса с открытым файлом. Информация фактически присутствует
только в памяти ядра в течение того времени, когда процесс имеет откры-
открытый файл;
□ элемент каталога — этот объект хранит информацию о связи записи в ка-
каталоге с файлом (иными словами, конкретное имя файла). Каждая диско-
дисковая файловая система хранит эту информацию на диске специфичным для
себя способом.
Рис. 12.2 иллюстрирует на простом примере, как процессы взаимодействуют
с файлами. Три разных процесса открыли один файл, причем два из них
пользуются одной жесткой ссылкой. В этом случае каждый из трех процессов
работает с собственным объектом, в то время как требуются только два объ-
объекта "элемент каталога", по одному на каждую жесткую ссылку. Оба объекта
"элемент каталога" ссылаются на один объект "индексный дескриптор", ко-
который идентифицирует объект-суперблок и вместе с ним обычный файл на
диске
Рис. 12.2. Взаимодействие между процессами и объемами VFS
Помимо предоставления общего интерфейса ко всем реализациям файловых
систем, VFS играет важную роль, имеющую отношение к производительно-
производительности системы. Самые "свежие" объекты "элемент каталога" содержатся в кэше
диска, называемом кэшем элементов каталога. Это ускоряет трансляцию
пути к файлу в объект "индексный дескриптор" последнего компонента пути.
Вообще говоря, кэш диска является программным механизмом, позволяю-
позволяющим ядру держать в оперативной памяти информацию, которая в нормальной
ситуации хранится на диске. Это позволяет быстрее обращаться к данным,
избегая дисковых операций, которые сами по себе выполняются медленно.
Обратите внимание на отличие дискового кэша от аппаратного кэша или кэ-
кэша памяти, которые не имеют никакого отношения к дискам и другим уст-
устройствам. Аппаратный кэш — быстрая статическая оперативная память, ко-
которая ускоряет выполнение запросов к более медленной динамической опе-
оперативной памяти (см. главу 2). Кэш памяти— программный механизм,
введенный для того, чтобы обойти аллокатор памяти ядра (см. главу 8).
Кроме кэша элементов каталога и кэша индексных дескрипторов, в Linux
применяются и другие дисковые кэши. Самый важный из них — кэш стра-
страниц, подробно описывается в главе 15.
Системные вызовы, обрабатываемые VFS
В табл. 12.1 перечислены системные вызовы, относящиеся к файловым сис-
системам, обычным файлам, каталогам и символьным ссылкам. Некоторые дру-
другие системные вызовы, обрабатываемые VFS, такие как iopermo, ioctio,
pipe () и mknod (), имеют отношение к файлам устройств и каналам. Они об-
обсуждаются в следующих главах. Еще одна группа системных вызовов, обра-
обрабатываемых VFS, В которую ВХОДЯТ, например, socket (), connect () И bind (),
относится к сокетам и применяется в реализации сетей. Некоторые из слу-
служебных процедур ядра, соответствующих системным вызовам, перечислен-
перечисленным в табл. 12.1, обсуждаются либо в этой главе, либо в главе 18.
Таблица 12.1. Некоторые системные вызовы, обрабатываемые VFS
Название Описание
mount (), umount (), umount2 () Монтирование/размонтирование
файловой системы
sysf s () Чтение информации о файловой
системе
statf s (), f statf s (), statf s64 (), Получение статистики, связанной
f statf s64 (), ustat () с файловой системой
chroot (), pivot_root () Смена корневого каталога
chdir (), f chdir (), getcwd () Операции с текущим каталогом
mkdir (), rmdir () Создание и уничтожение каталогов
getdents (), getdents64 (), readdir (), link (), Операции с записями в каталоге
unlink(), rename(), lookup_dcookie()
readlink (), symlink () Операции с гибкими ссылками
chown (), f chown (), lchown (), chownl6 (), Смена владельца файла
fchownlб(), lchownl6()
chmod (), f chmod (), utime () Изменение атрибутов файла
stat (), f stat (), lstat (), access (), Чтение состояния файла
oldstat (), oldf stat (), oldlstat (), stat64 (),
Istat64(), fstat64()
open (), close (), creat (), umask () Открытие, закрытие и создание
файлов
dup (), dup2 (), f cnti (), f cnti64 () Операции с дескриптором файла
select (), poll () Ожидание событий на наборе
дескрипторов файлов
truncate (), ftruncate (), truncate64 (), Изменение размера файла
ftruncate64()
Таблица 12.1 (окончание)
Название Описание
lseek (), _llseek () Изменение положения указателя
файла
read (), write (), readv (), sendf ile (), Выполнение файловых операций
sendf ile64 (), readahead () ввода/вывода
io_setup (), io_submit (), io_getevents (), Асинхронный ввод/вывод (допускает
io_cancel (), io_destroy () несколько ожидающих выполнения
~ запросов на чтение и запись)
pread64 (), pwrite64 () Установка указателя в файле
и обращение к нему
гптар (), ттар2 (), munmap (), madvise (), Работа с отображением файла
mincore(), remap_file_pages в память
fdatasync (), f sync (), sync (), msync () Синхронизация файловых данных
flock() Блокировка файла
setxattr (), lsetxattr (), f setxattr (), Операции с расширенными
getxattr(), Igetxattr(), fgetxattr(), атрибутами файла
listxattr(), llistxattr(), fllistxattr(),
removexattr(), lremovexattr(),
fremovexattr()
Ранее мы уже говорили, что VFS является слоем между программами и кон-
конкретными файловыми системами. Однако в отдельных случаях VFS может
самостоятельно выполнить файловую операцию, не обращаясь к процедуре
более низкого уровня. Например, когда процесс закрывает открытый файл,
никакие операции с файлом на диске, как правило, не требуются, и VFS про-
просто освобождает соответствующий файловый объект. Аналогичным образом,
когда системный вызов lseek о перемещает файловый указатель (который
является атрибутом, относящимся к взаимодействию процесса с открытым
файлом), для VFS достаточно модифицировать лишь соответствующий фай-
файловый объект без обращения к файлу на диске. Следовательно, у VFS нет не-
необходимости вызывать специальную процедуру. В определенном смысле
можно считать VFS универсальной файловой системой, которая, в случае не-
необходимости, опирается на специфические файловые системы.
Структуры данных VFS
Каждый объект VFS хранится в соответствующей структуре данных, которая
включает в себя как атрибуты объекта, так и указатель на таблицу методов
объекта. Ядро может динамически модифицировать методы объекта и, следо-
вательно, задавать его специальное поведение. В следующих разделах под-
подробно рассматриваются объекты VFS и их взаимоотношения.
Суперблоки
Объект-суперблок состоит из структуры super_biock, поля которой описаны
в табл. 12.2.
Таблица 12.2. Поля суперблока
Тип Поле Описание
struct list_head slist Указатели на список суперблоков
devt sdev Идентификатор устройства
unsigned long s_blocksize Размер блока в байтах
unsigned long s_old_blocksize Размер блока в байтах по инфор-
информации, полученной от драйвера
блочного устройства
unsigned char s_blocksize_bits Размер блока в битах
unsigned char sdirt Флаг, указывающий, что суперблок
"грязный" (модифицированный)
unsigned long long s_maxbytes Максимальный размер файлов
struct f ile_system_type * s_type Тип файловой системы
struct super_operations * s_op Методы суперблока
struct dquot_operations * dq_op Методы для обработки дисковых
квот
struct quotactl_ops * s_qcop Методы для администрирования
дисковых квот
struct export_ope rat ions * s_export Операции экспорта, применяемые
в сетевых файловых системах
unsigned long s_f lags Флаги монтирования
unsigned long smagic Магическое число файловой сис-
системы
struct dentry * s_root Объект "элемент каталога" корне-
корневого каталога файловой системы
struct rw_semaphore s_umount Семафор, используемый для раз-
монтирования
struct semaphore s_lock Семафор суперблока
int s_count Счетчик ссылок
Таблица 12.2 (продолжение)
Тип Поле Описание
int ssyncing Флаг, показывающий, что индекс-
индексные дескрипторы суперблока в
данный момент синхронизируются
int s_need_sync_fs Флаг, используемый во время син-
синхронизации смонтированной фай-
файловой системы суперблока
atomict s_active Вторичный счетчик ссылок
void * s_security Указатель на структуру безопас-
безопасности суперблока
struct xattr_handler ** s_xattr Указатель на структуру расширен-
расширенных атрибутов суперблока
struct list_head s_inodes Список всех индексных
дескрипторов
struct list_head s_dirty Список модифицированных
индексных дескрипторов
struct list_head s_io Список индексных дескрипторов,
ждущих записи на диск
struct hlist_head s_anon Список анонимных элементов ка-
каталога для обработки удаленных
сетевых файловых систем
struct listhead s_f iles Список файловых объектов
struct blockdevice * sjodev Указатель на дескриптор драйвера
блочного устройства
struct list_head s_instances Указатели на список объектов
"суперблок11 в файловой системе
данного типа
struct quotainfo s_dqout Дескриптор дисковой квоты
int s_frozen Флаг, используемый при
"замораживании" файловой
системы (переводе ее во внутренне
непротиворечивое состояние)
wait_queue_head_t s_wait_unf rozen Очередь ожидания, в которой про-
процессы приостанавливаются до тех
пор, пока файловая система не
будет "разморожена"
char [ ] s_id Имя блочного устройства, содер-
содержащего суперблок
void * s_fsinfo Указатель на информацию
о суперблоке конкретной файловой
системы
Таблица 12.2 (окончание)
Тип Поле Описание
struct semaphore s_vf s_rename_sem Семафор, используемый VFS
при переименовании файлов
в каталогах
u32 stimegran Точность отметки времени
(в наносекундах)
Все суперблоки объединены в циклический двунаправленный список. Пер-
Первый элемент этого списка представлен переменной superjoiock, а поле slist
суперблока содержит указатели на соседние элементы списка. Спин-
блокировка sbiock защищает список от попыток одновременного обращения
в многопроцессорных системах.
Поле sfsinfo является указателем на информацию о суперблоке кон-
конкретной файловой системы. Например, как мы увидим в главе 18, если
объект-суперблок относится к файловой системе Ext2, это поле указывает на
структуру ext2_sb_inf о, которая содержит битовые маски распределения про-
пространства на диске и прочие данные, не имеющие отношения к общей файло-
файловой модели VFS.
Вообще говоря, данные, на которые указывает поле s_f sinfo, являются ин-
информацией, взятой с копии диска в памяти, созданной из соображений эф-
эффективности. Каждая дисковая файловая система должна читать и обновлять
свои битовые карты распределения свободного места, чтобы выделять и ос-
освобождать блоки на диске. VFS позволяет этим файловым системам работать
непосредственно с полем sfsinfo суперблока в памяти, не обращаясь к
диску.
Впрочем, такой подход создает одну проблему. Суперблок VFS может ока-
оказаться рассинхронизированным с соответствующим суперблоком на диске.
Это приводит к необходимости введения флага sdirt, который показывает,
является ли суперблок "грязным", т. е. следует ли обновлять данные на диске.
Отсутствие синхронизации приводит к известной проблеме разрушенной
файловой системы, которая возникает, когда электропитание неожиданно
выключается, не давая пользователю осуществить корректный останов сис-
системы. Как мы увидим в разд. "Запись грязных страниц на диск" в главе 75,
Linux минимизирует эту проблему, периодически копируя все "грязные" су-
суперблоки на диск.
Методы, связанные с объектом "суперблок", называются операциями супер-
суперблока. Они описаны в структуре superoperations, адрес которой хранится
в поле sop.
В каждой файловой системе могут быть определены собственные операции
суперблока. Когда VFS нужно вызвать один из них, например readinode (),
она выполняет следующий код:
sb->s_op->read_inode(inode);
Здесь sb содержит адрес соответствующего суперблока. Поле readinode таб-
таблицы superoperations хранит адрес нужной функции, которая и вызывается.
Кратко опишем операции суперблока, реализующие операции высокого
уровня, такие как удаление файлов или монтирование дисков. Они приведе-
приведены в том порядке, в каком хранятся в таблице superoperations:
□ aiiocinode(sb) — выделяет место для объекта "индексный дескриптор",
в том числе и для данных о конкретной файловой системе;
□ destroyinode(inode) — уничтожает объект "индексный дескриптор" и
данные о конкретной файловой системе;
□ readinode (inode) — заполняет поля объекта "индексный дескриптор",
переданного в качестве параметра, данными, хранящимися на диске; поле
iino объекта "индексный дескриптор" идентифицирует конкретный ин-
индексный дескриптор файловой системы, который следует прочитать с
диска;
□ dirtyinode (inode) — вызывается, когда индексный дескриптор помечен
как модифицированный (грязный). Используется файловыми системами,
такими как ReiserFS и Ext3, для обновления журнала файловой системы на
диске;
□ writeinode (inode, flag) — записывает в индексный дескриптор файло-
вой системы содержимое объекта "индексный дескриптор", переданного в
качестве параметра; поле iino объекта "индексный дескриптор" иденти-
идентифицирует конкретный индексный дескриптор файловой системы на диске.
Параметр flag показывает, должна ли операция ввода/вывода быть син-
синхронной;
□ putinode (inode) — вызывается при освобождении индексного дескрип-
дескриптора (уменьшении на единицу его счетчика ссылок) для выполнения опе-
операций, специфических для файловой системы;
О dropinode(inode) — вызывается непосредственно перед уничтожением
индексного дескриптора, т. е. когда последний пользователь освобождает
индексный дескриптор. Файловые системы, в которых реализован этот ме-
метод, обычно используют genericdropinode (). Эта функция удаляет все
ссылки на индексный дескриптор из структур данных VFS и, если индекс-
индексный дескриптор больше не присутствует в каталоге, вызывает метод су-
суперблока deieteinode, который удаляет индексный дескриптор из файло-
файловой системы;
□ deieteinode(inode) — вызывается, когда индексный дескриптор должен
быть уничтожен. Удаляет индексный дескриптор VFS из памяти, а также
данные файла и метаданные с диска;
□ putsuper (super) — освобождает объект-суперблок, переданный в качест-
качестве параметра (потому что соответствующая файловая система размонти-
размонтирована);
□ writesuper (super) — записывает в суперблок файловой системы содер-
содержимое указанного объекта;
□ sync_fs(sb, wait) — вызывается при сбросе на диск содержимого буфе-
буферов файловой системы для обновления специфичных для нее структур
данных на диске (используется в журналируемых файловых системах);
П writesuperiockfs (super) — блокирует изменения в файловой системе и
записывает в суперблок файловой системы содержимое указанного объек-
объекта. Этот метод вызывается, когда файловая система "заморожена", напри-
например, драйвером LVM (Logical Volume Manager, Менеджер логических
томов);
□ uniockfs (super)— снимает блокировку изменений файловой системы,
установленную методом writesuperiockf s;
□ statfs (super, buf) — возвращает статистику по файловой системе в бу-
буфере buf;
□ remountf s (super, flags, data) — заново монтирует файловую систему с
новыми настройками (вызывается, когда какая-то настройка монтирования
должна быть изменена);
□ ciearinode(inode) — вызывается, когда уничтожается индексный деск-
дескриптор на диске для выполнения операций, специфических для файловой
системы;
□ umountbegin (super) — отменяет операцию монтирования, поскольку была
запущена соответствующая операция размонтирования (применяется
только в сетевых файловых системах);
□ show_options(seq_file, vfsmount) — ИСПОЛЬЗуетсЯ ДЛЯ ВЫВОДа настроек
файловой системы;
□ quota_read(super, type, data, size, offset) — ИСПОЛЬЗуетсЯ системой
квот для чтения данных из файла, в котором указаны ограничения, уста-
установленные в этой файловой системе2;
2 Система квот определяет для каждого пользователя или группы ограничения на объем простран-
пространства, разрешенного к использованию в данной файловой системе.
□ quota_write (super, type, data, size, offset) — ИСПОЛЬЗуетСЯ системой
квот для записи данных в файл, в котором указаны ограничения, установ-
установленные в этой файловой системе.
Перечисленные методы доступны во всех допустимых файловых системах,
однако в каждой конкретной файловой системе имеется лишь какое-то их
подмножество, а поля, соответствующие нереализованным методам, прирав-
приравнены К NULL.
Обратите внимание на отсутствие метода getsuper для чтения суперблока.
В самом деле, как ядро стало бы вызывать метод объекта, который еще толь-
только предстоит прочитать с диска? Мы найдем эквивалентный метод getsb
у другого объекта, описывающего тип файловой системы (см.
разд. "Регистрация типа файловой системы " далее в этой главе).
Индексный дескриптор
Вся информация, необходимая файловой системе для работы с файлом, нахо-
находится в структуре данных, называемой индексным дескриптором. Имя фай-
файла— это произвольно выбранная метка, которая может измениться, а ин-
индексный дескриптор уникален для каждого файла и остается неизменным все
то время, что файл существует. Объект "индексный дескриптор", располо-
расположенный в памяти, представляет собой структуру inode, поля которой описаны
в табл. 12.3.
Таблица 12.3. Поля объекта "индексный дескриптор"
Тип Поле Описание
struct hlist_node i_hash Указатели на хеш-список
struct listjnead i_list Указатели на список, представляю-
представляющий текущее состояние индексного
дескриптора
struct list_head i_sb_list Указатели на индексные дескрипто-
дескрипторы суперблока
struct list_head i_dentry Голова списка объектов "элемент
каталога", ссылающихся на этот
индексный дескриптор
unsigned long i_ino Номер индексного дескриптора
atomic_t i_count Счетчик обращений
umode_t imode Тип файла и права доступа
unsigned int i_link Количество жестких ссылок
uidt i_uid Идентификатор владельца
Таблица 12.3 (продолжение)
Тип Поле Описание
gid_t i_gid Идентификатор группы
dev_t i_rdev Идентификатор реального
устройства
lof f_t isize Длина файла в байтах
struct timespec iatime Время последнего чтения файла
struct timespec imtime Время последней записи в файл
struct timespec ictime Время последнего изменения
индексного дескриптора
unsigned int i_blkbits Размер блока в битах
unsigned long i_blksize Размер блока в байтах
unsigned long i_version Номер версии, автоматически увели-
увеличиваемый после каждого использо-
использования
unsigned long ijolocks Количество блоков файла
unsigned short i_bytes Количество байтов в последнем
блоке файла
unsigned char i_sock Ненулевое значение, если файл
является сокетом
spinlock_t i_lock Спин-блокировка, защищающая
некоторые поля индексного
дескриптора
struct semaphore i_sem Семафор индексного дескриптора
struct rw_semaphore i_alloc_sem Семафор чтения/записи для предот-
предотвращения конфликтов в операциях
прямого ввода/вывода файлов
struct inode_operations * i_op Операции индексного дескриптора
struct file_operations * i_fop Файловые операции по умолчанию
struct super_block * i_sb Указатель на суперблок
struct f ile_lock * if lock Указатель на список блокировок
файлов
struct address_space * ijnapping Указатель на объект address_space
(см. главу 15)
struct address_space i_data Объект address_space данного
файла
struct dquot * [] i_dquot Дисковые квоты индексного дескрип-
дескриптора
Таблица 12.3 (окончание)
Тип Поле Описание
struct list_head i_devices Указатели для списка индексных
дескрипторов для конкретного сим-
символьного или блочного устройства
(см. главу 13)
struct pipe_inode_info * i_pipe Используется, если файл является
каналом (см. главу 19)
struct block_device * i_bdev Указатель на драйвер блочного
устройства
struct cdev * i_cdev Указатель на драйвер символьного
устройства
int icindex Индекс файла устройства в пределах
группы младших номеров
u32 i_generation Номер версии индексного
дескриптора (используется
в некоторых файловых системах)
unsigned long i_dnotif y_mask Битовая маска событий уведомления
каталога
struct dnotify_struct * i_dnotif у Применяется для уведомления
каталога
unsigned long i_state Флаги состояния индексного
дескриптора
unsigned long dirtied_when Время "загрязнения" индексного
дескриптора (в тиках)
unsigned int i_f lags Флаги монтирования файловой
системы
atomic_t i_writecount Счетчик обращений для пишущих
процессов
void * i_security Указатель на структуру безопасности
void* u.generic_ip Указатель на частные данные
seqcount_t i_size_seqcount Последовательный счетчик, приме-
применяемый в SMP-системах для получе-
получения согласованных значений поля
i_size
Каждый объект "индексный дескриптор" дублирует некоторые данные, со-
содержащиеся в индексном дескрипторе на диске, например, количество бло-
блоков, выделенных под файл. Когда значение поля istate равно idirtysync,
IDIRTYDATASYNC ИЛИ I_DIRTY_PAGES, ИНДвКСНЫЙ ДвСКрИПТОр ЯВЛЯвТСЯ "грЯЗ-
ным", т. е. соответствующий индексный дескриптор на диске должен быть
обновлен. Макрос idirty может быть использован для проверки сразу всех
этих трех флагов. Кроме того, поле istate может принимать значения:
ilock (объект "индексный дескриптор" участвует в операции ввода/вывода),
ifreeing (объект "индексный дескриптор" освобождается), iclear (содер-
(содержимое объекта "индексный дескриптор" утратило смысл) и inew (объект
"индексный дескриптор" создан, но еще не заполнен данными из индексного
дескриптора, хранящегося на диске).
Каждый объект "индексный дескриптор" обязательно присутствует в одном
из следующих циклических двунаправленных списков (и во всех случаях
указатели на соседние элементы хранятся в поле ilist):
□ список допустимых свободных индексных дескрипторов, как правило, тех,
которые отражают индексный дескриптор на диске и не используются
в данный момент ни одним процессом. Эти индексные дескрипторы не яв-
являются "грязными", а их поля icount установлены в ноль. На первый и
последний элементы этого списка указывают соответственно поля next и
prev переменной inodeunused. Этот список используется как кэш диска;
□ список используемых индексных дескрипторов, т. е. тех, которые отража-
отражают индексный дескриптор на диске и используются в каких-то процессах.
Эти индексные дескрипторы не являются "грязными", а их поля icount
имеют положительные значения. На первый и последний элементы указы-
указывает переменная inodeinuse;
□ список "грязных" индексных дескрипторов. На первый и последний эле-
элементы указывает поле sdirty соответствующего суперблока.
Каждый из упомянутых списков связывает поля ilist соответствующих
объектов "индексный дескриптор".
Кроме того, каждый объект "индексный дескриптор" также включен в некий
циклический двунаправленный список, который имеется в каждой файловой
системе и на который указывает поле sinodes объекта "суперблок". При
этом поле isbiist объекта "индексный дескриптор" содержит указатели на
соседние элементы этого списка.
Наконец, объекты "индексный дескриптор" содержатся в хеш-таблице по
имени inodehashtabie. Эта таблица ускоряет поиск объекта "индексный де-
дескриптор", когда ядру известны как номер индексного дескриптора, так и ад-
адрес суперблока, соответствующего файловой системе, в которой находится
данный файл. Поскольку хеширование может привести к коллизиям, объект
"индексный дескриптор" имеет поле ihash, содержащее указатели на после-
последующий и предыдущий индексные дескрипторы, хешированные в ту же по-
позицию. Таким образом, это поле создает двунаправленный список этих ин-
индексных дескрипторов.
Методы, связанные с объектом "индексный дескриптор", называются опера-
операциями индексного дескриптора. Они описаны в структуре inodeoperations,
адрес которой хранится в поле i_op. Далее приводится список операций ин-
индексного дескриптора в том порядке, в каком они перечислены в таблице
inode_operations:
□ create (dir, dentry, mode, nameidata) — создает НОВЫЙ индексный деск-
риптор на диске для обычного файла, связанного с объектом "элемент ка-
каталога" в указанном каталоге;
□ lookup (dir, dentry, nameidata) — Ищет В каталоге индексный деСКрИП-
тор, соответствующий имени файла в объекте "элемент каталога";
□ link(old_dentry, dir, new_dentry) — СОЗДает НОВуЮ жесткую ССЫЛКу на
файл, указанный в параметре oiddentry и расположенный в каталоге dir.
Имя новой жесткой ссылки указано в параметре newdentry;
□ unlink (dir, dentry) — удаляет жесткую ссылку на файл, определяемый
объектом "элемент каталога" и расположенный в указанном каталоге;
□ symlink(dir, dentry, symname) — СОЗДает НОВЫЙ индексный дескриптор
для символьной ссылки, связанной с объектом "элемент каталога" в ука-
указанном каталоге;
□ mkdir(dir, dentry, mode) — СОЗДает НОВЫЙ Индексный дескриптор ДЛЯ
каталога, связанного с объектом "элемент каталога" в указанном каталоге;
□ rmdir(dir, dentry) — удаляет из каталога подкаталог, имя которого со-
содержится в объекте "элемент каталога";
□ mknod(dir, dentry, mode, rdev) — СОЗДает НОВЫЙ индексный деСКрИПТОр
на диске для специального файла, связанного с объектом "элемент катало-
каталога" в указанном каталоге. Параметры mode и rdev указывают, соответст-
соответственно, тип файла и старший и младший номера устройства;
□ rename (old_dir, old_dentry, new_dir, new_dentry) — переносит файл,
идентифицируемый параметром oiddentry, из каталога olddir в каталог
newdir. Новое имя файла содержится в объекте "элемент каталога", на ко-
который указывает параметр newdentry;
□ readlink (dentry, buffer, buflen) — копирует путь К файлу, COOTBeTCT-
вующий символьной ссылке, указанной элементом каталога, в область па-
памяти пространства пользователя, указанную параметром buffer;
□ follow_link(inode, nameidata) — транслирует СИМВОЛЬНУЮ ССЫЛКу, ука-
занную объектом "индексный дескриптор". Если эта ссылка является от-
относительным путем, операция просмотра начинается с каталога, указанно-
указанного во втором параметре;
□ put_iink(dentry, nameidata) — освобождает все временные структуры
данных, выделенные методом foiiowiink для трансляции символьной
ссылки;
□ truncate (inode) — изменяет размер файла, связанного с индексным деск-
дескриптором. До вызова этого метода необходимо записать в поле isize
объекта "индексный дескриптор" новый размер файла;
□ permission (inode, mask, nameidata) — проверяет, разрешен ли указанный
режим доступа для файла, связанного с индексным дескриптором;
□ setattr (dentry, iattr) — возбуждает событие "изменено" после установ-
установки атрибутов индексного дескриптора;
□ getattr(mnt, dentry, kstat) — ИСПОЛЬЗуетСЯ В некоторых фаЙЛОВЫХ СИС-
темах для чтения атрибутов индексного дескриптора;
□ setxattr (dentry, name, value, size, flags) —устанавливает "расширен-
ный атрибут" индексного дескриптора (расширенные атрибуты хранятся
в блоках диска за пределами любого индексного дескриптора);
□ getxattr (dentry, name, buffer, size) — читает расширенный атрибут
индексного дескриптора;
□ listxattr (dentry, buffer, size) — читает полный список имен расши-
расширенных атрибутов;
□ removexattr (dentry, name) —удаляет расширенный атрибут.
Перечисленные методы доступны всем индексным дескрипторам во всех до-
допустимых файловых системах, однако в каждой конкретной файловой систе-
системе имеется лишь какое-то их подмножество, а поля, соответствующие нереа-
нереализованным методам, установлены в null.
Файловые объекты
Файловый объект описывает работу процесса с файлом, который он открыл.
Этот объект создается в момент открытия файла и представлен структурой
file, поля которой описаны в табл. 12.4. Обратите внимание, что файловые
объекты не имеют образа на диске и, следовательно, структура file не имеет
поля, которое указывало бы, что объект "грязный", т. е. был модифицирован.
Таблица 12.4. Поля файлового объекта
Тип Поле Описание
struct list_head f_list Указатели на список файловых объектов
данной файловой системы
Таблица 12.4 (окончание)
Тип Поле Описание
struct dentry * f_dentry Объект "элемент каталога", связанный
с файлом
struct vfsmount * f_vf smnt Смонтированная файловая система,
содержащая данный файл
struct f ile_ope rat ions * f_op Указатель на таблицу файловых опера-
операций
atomic_t f_count Счетчик ссылок файлового объекта
unsigned int f_f lags Флаги, указанные при открытии файла
modet fmode Режим доступа для процесса
int ferror Код ошибки сетевой операции записи
lof ft f_pos Текущее смещение от начала файла
(файловый указатель)
struct f own_struct fowner Данные для уведомления о событии
"ввод/вывод" с помощью сигналов
unsigned int fuid Идентификатор пользователя
unsigned int fgid Идентификатор группы
struct f ile_ra_state f_ra Состояние опережающего чтения файла
(см. главу 16)
sizet f_maxcount Максимальное количество байтов, кото-
которые могут быть прочитаны или записаны
в ходе одной операции (в настоящее
время равно 231-1)
unsigned long f_version Номер версии, автоматически увеличи-
увеличиваемый после каждого использования
void * f_security Указатель на структуру безопасности
файлового объекта
void * private_data Указатель на данные, специфичные для
файловой системы или драйвера устрой-
устройства
struct list_head f_ep_links Голова списка процессов, ожидающих
некоторого события для данного файла
spinlock_t f_ep_lock Спин-блокировка для защиты списка
f_ep_lock
struct address_space* fmapping Указатель на объект адресного простран-
пространства файла (см. главу 15)
Самой важной информацией, хранящейся в объекте, является файловый ука-
указатель, текущая позиция в файле, с которой начнется следующая операция
чтения/записи. Поскольку к одному файлу одновременно могут обратиться
несколько процессов, указатель файла необходимо хранить в файловом объ-
объекте, а не в объекте "индексный дескриптор".
Файловые объекты выделяются через slab-кэш, называемый flip. Адрес деск-
дескриптора кэша хранится в переменной f iipcachep. Поскольку существует ог-
ограничение на количество выделяемых файловых объектов, переменная
f ilesstat хранит в поле maxf iles максимальное количество файловых объ-
объектов, которые могут быть выделены, т. е. максимальное количество файлов,
к которым можно обратиться в данной системе одновременно.
( Примечание )
Функция filesinit (), выполняемая во время инициализации ядра, устанав-
устанавливает значение поля maxfiles равным одной десятой объема доступного
ОЗУ (в килобайтах), но системный администратор может изменить этот пара-
параметр, отредактировав файл /proc/sys/file-max. Более того, суперпользователь
всегда может получить файловый объект, даже если уже выделено столько
файлов, сколько указано в поле maxf iles.
"Используемые" файловые объекты собраны в нескольких списках, разме-
размещенных в суперблоках файловой системы. Каждый суперблок содержит в
поле s_f iles голову списка файловых объектов. Таким образом, объекты для
файлов, принадлежащих разным файловым системам, включены в разные
списки. Указатели на предыдущий и следующий элементы списка хранятся в
поле flist файлового объекта. Спин-блокировка filesiock защищает спи-
списки s_f iles от попыток одновременного обращения к ним в многопроцессор-
многопроцессорных системах.
Поле fcount файлового объекта является счетчиком ссылок, который указы-
указывает количество процессов, использующих файловый объект (однако не сле-
следует забывать, что "легковесные" процессы с флагом clonefiles используют
одну таблицу, в которой перечислены открытые файлы, т. е. они обращаются
к одним и тем же файловым объектам). Счетчик также увеличивается, когда
файловые объекты используются самим ядром, например, когда объект зано-
заносится в список, или когда производится системный вызов dup ().
Когда система VFS должна открыть файл от имени какого-то процесса, она
вызывает функцию getemptyfiipo для выделения нового файлового объек-
объекта. ФуНКЦИЯ, В СВОЮ Очередь, ВЫЗЫВает kmem_cache_alloc(), чтобы ПОЛучИТЬ
свободный файловый объект из кэша filp, а затем инициализирует поля объ-
объекта следующим образом:
memset(f, 0, sizeof(*f));
INIT_LIST_HEAD(&f->f_ep_links);
spin_lock_init(&f->f_ep_lock);
atomic_set(&f->f_count, 1);
f->f_uid = current->fsuid;
f->f_gid = current->fguid;
f-->f_owner.lock = RW_LOCK_UNLOCK;
INIT_LIST_HEAD(&f->f_list) ;
f->f_maxcount = INT_MAX;
Как было сказано ранее, каждая файловая система имеет собственный набор
файловых операций, выполняющих такие действия, как чтение и запись фай-
файла. Когда ядро загружает индексный дескриптор с диска в память, оно сохра-
сохраняет указатель на эти файловые операции в структуре f ileoperations, адрес
которой хранится в поле ifop объекта "индексный дескриптор". Когда неко-
некоторый процесс открывает файл, VFS инициализирует поле fop нового фай-
файлового объекта адресом, хранящимся в индексном дескрипторе, чтобы в по-
последующих обращениях к файловым операциям могли быть использованы
эти функции. Если возникнет необходимость, VFS сможет впоследствии
модифицировать список файловых операций, сохранив в поле fop новое
значение.
Далее перечисляются файловые операции в том порядке, в каком они появ-
появляются В таблице f ileoperations:
П1 iiseek(fiie, offset, origin)—обновляет позицию файлового указателя;
□ read (file, buf, count, offset) — читает count байтов файла, начиная с
позиции offset. После этого значение offset (которое обычно соответст-
соответствует файловому указателю) увеличивается;
□ aio_read(req, buf, len, pos) — запускает асинхронную операцию вво-
ввода/вывода, чтобы прочитать len байтов в буфер buf, начиная с позиции pos
в файле. Эта операция введена для поддержки системного вызова
io_submit ();
□ write (file, buf, count, offset) — записывает count байтов в файл, на-
начиная с позиции offset. После этого значение offset (которое обычно со-
соответствует файловому указателю) увеличивается;
□ aio_write(req, buf, len, pos) — запускает асинхронную операцию вво-
ввода/вывода, чтобы записать len байтов из буфера в файл, в позицию pos;
□ readdir(dir, dirent, fiiidir)—возвращает следующий элемент катало-
каталога в dirent. Параметр fiiidir содержит адрес служебной функции, кото-
которая извлекает поля элемента каталога;
□ poll (file, poiitabie) — проверяет файловую активность и переходит
в режим ожидания, пока не произойдет какое-либо соответствующее со-
событие;
□ ioctl(inode, file, cmd, arg) — отправляет команду соответствующему
аппаратному устройству. Этот метод относится только к файлам уст-
устройств;
□ unlocked_ioctl (file, cmd, arg) — аналогичен методу ioctl, НО не ИСПОЛЬ-
зует глобальную блокировку ядра (см. главу 5). Ожидается, что этот новый
метод будет реализован во всех драйверах устройств и во всех файловых
системах вместо метода iocti;
□ compatiocti (file, cmd, arg)— метод, применяемый для реализации
32-битового системного вызова ioctl () в 64-разрядном ядре;
П mmap(fiie, vma) — выполняет отображение файла в адресное пространст-
пространство процесса (см. разд. "Отображение в память" в главе 16);
П open(inode, file) — открывает файл, создавая новый файловый объект и
связывая его с соответствующим объектом "индексный дескриптор" (см.
разд. "Системный вызов ореп() " далее в этой главе);
П flush (file) — вызывается после закрытия ссылки на открытый файл. Ре-
Реальные действия этого метода зависят от файловой системы;
□ release (inode, file) — освобождает объект. Вызывается, когда закрыва-
закрывается последняя ссылка на открытый объект, т. е. когда поле fcount фай-
файлового объекта становится равным нулю;
□ fsync(fiie, dentry, flag) — принудительно записывает файл, т. е. запи-
записывает все кэшированные данные на диск;
П1 aio_fsync(req, flag) — запускает асинхронную операцию сброса кэши-
рованных данных на диск;
□ fasync (fd, file, on) — включает или выключает сигнальное оповещение
о вводе/выводе;
П lock (file, cmd, fileiock) — блокирует указанный файл (см. разд. "Бло-
"Блокировка фатов" далее в этой главе);
П readv(fiie, vector, count, offset)—выполняет побайтовое чтение фай-
файла и помещает результат в буферы, описанные параметром vector; количе-
количество буферов указано в параметре count;
□ writev(file, vector, count, offset) ВЫПОЛНЯеТ Побайтовую Запись
в файл из буферов, описанных параметром vector; количество буферов
указано в параметре count;
П sendfile (in_file, offset, count, file_send_actor, out_file) —пересы-
лает данные из inf ile в outf ile (введен для поддержки системного вы-
вызова sendfile ());
□ sendpage (file, page, offset, size, pointer, fill) —пересылает данные
из file в кэш страницы page. Это низкоуровневый, используемый в
sendf Не О и в коде поддержки сокетов;
□ get_unmapped_area(file, addr, len, offset, flags) — получает неис-
пользуемый диапазон адресов для отображения файла в память;
□ checkfiags (flags) — метод, вызываемый служебной процедурой систем-
системного вызова f cnti () для выполнения дополнительных проверок при уста-
установке флагов состояния файла (команда fsetfl). В настоящее время ис-
используется только в сетевых файловых системах;
□ dir_notify(fiie, arg) — метод, вызываемый служебной процедурой сис-
системного вызова f cnti () при включении оповещения об изменениях в ката-
каталоге (команда fnotify). В настоящее время используется только в сетевой
файловой системе CIFS (Common Internet File System, Общая файловая
система Интернета);
□ flock (file, flag, lock) — используется для настройки системного вызо-
вызова flock. Ни одна из официальных файловых систем Linux этим методом
не пользуется.
Эти методы доступны для всех допустимых типов файлов. Однако только
определенное их подмножество относится к конкретному типу файла; поля,
соответствующие нереализованным методам, устанавливаются в null.
Элемент каталога
Мы уже говорили, что VFS рассматривает каждый каталог как файл, содер-
содержащий список файлов и других каталогов. В главе 18 мы обсудим реализа-
реализацию каталогов в конкретных файловых системах. Однако, после того как
запись из каталога прочитана в память, VFS преобразует ее в объект "элемент
каталога", основанный на структуре dentry, поля которой представлены в
табл. 12.5. Ядро создает объект "элемент каталога" для каждого элемента пу-
пути, просматриваемого процессом, а объект "элемент каталога" связывает эле-
элемент с соответствующим индексным дескриптором. Например, при просмот-
просмотре пути /tmp/test ядро создает объект "элемент каталога" для корневого ката-
каталога (/), второй объект "элемент каталога" — для записи tmp в корневом
каталоге, а третий — для записи test каталога /tmp.
Обратите внимание, что объекты "элемент каталога" не имеют образов на
диске, и, следовательно, у структуры dentry нет поля, которое отмечало бы,
что объект был модифицирован. Объекты "элемент каталога" хранятся в кэше
slab-аллокатора, дескриптором которого является dentrycache. Таким обра-
образом, объекты "элемент каталога" создаются и уничтожаются методами
kmem_cache_alloc () И kmem_cache_f гее ().
Таблица 12.5. Поля объекта "элемент каталога"
Тип Поле Описание
atomic_t d_count Счетчик обращений к объекту "элемент
каталога"
unsigned int d_f lags Флаги кэша элементов каталога
spinlock_t d_lock Спин-блокировка для защиты объекта
"элемент каталога"
struct inode * d_inode Индексный дескриптор, связанный
с файлом
struct dentry * d_parent Объект "элемент каталога" родительского
каталога
struct qstr d_name Имя файла
struct list_head d_iru Указатели на список неиспользуемых
элементов каталога
struct listhead d_child Для каталогов: указатели на список
элементов каталога в том же самом
родительском каталоге
struct list_head d_subdirs Для каталогов: голова списка элементов
каталога подкаталогов
struct list_head d_alias Указатели на список элементов каталога,
асссоциированных с тем же индексным
дескриптором (псевдонимов)
unsigned long d_time Используется методом d_revalidate
struct dentry_operations * d_op Методы элемента каталога
struct super_block * d_sb Объект-суперблок для файла
void * d_f sdata Данные, специфичные для файловой
системы
struct rcu_head d_rcu Дескриптор RCU, используемый при утили-
утилизации объектов "элемент каталога"
struct dcookie_struct * d_cookie Указатель на структуру, используемую
профайлерами ядра
struct hlist_node d_hash Указатель для списка в записи
хеш-таблицы
int d_mounted Для каталогов: счетчик файловых систем,
смонтированных на данном элементе
каталога
unsigned char [ j d_iname Короткое имя файла
Каждый объект "элемент каталога" может находиться в одном из четырех
состояний:
□ свободен — объект "элемент каталога" не содержит осмысленной инфор-
информации и не используется в VFS. Соответствующую область памяти кон-
контролирует slab-аллокатор;
□ не используется — объект "элемент каталога" в данный момент не исполь-
используется ядром. Счетчик обращений к объекту (dcount) равен нулю, но поле
dinode по-прежнему указывает на связанный объект "индексный деск-
дескриптор". Объект "элемент каталога" содержит полезную информацию, но
его содержимое может быть утрачено, если потребуется выделенная ему
память;
□ используется — объект "элемент каталога" используется ядром. Счетчик
обращений dcount содержит положительное значение, а поле dinode по-
прежнему указывает на связанный объект "индексный дескриптор". Объ-
Объект "элемент каталога" содержит полезную информацию и не может быть
уничтожен;
□ отрицательный— объект "индексный дескриптор", связанный с элемен-
элементом каталога, не существует, либо из-за того, что соответствующий ин-
индексный дескриптор был удален с диска, либо из-за того, что объект "эле-
"элемент каталога" был создан на основе пути к несуществующему файлу. По-
Поле dinode объекта "элемент каталога" равно null, но объект продолжает
существовать в кэше элементов каталога, чтобы последующие операции
обращения к тому же пути выполнялись быстрее. Термин "отрицатель-
"отрицательный" явно неудачен, поскольку ни о каких отрицательных значениях здесь
нет речи.
Методы, связанные с объектом "элемент каталога", называются операциями
элемента каталога. Они описаны в структуре dentryoperations, адрес кото-
которой хранится в поле d_op. Хотя в некоторых файловых системах определены
собственные методы элемента каталога, соответствующие поля обычно уста-
установлены в значение null, и VFS заменяется их функциями, установленными
по умолчанию. Далее эти методы приводятся в том порядке, в котором они
перечислены В таблице dent ry_ope rat ions:
П drevalidate (dentry, nameidata) — проверяет, является ЛИ объект "эле-
мент каталога" допустимым, прежде чем использовать его для трансляции
пути к файлу. Функция, принятая в VFS по умолчанию, не выполняет ни-
никаких действий, но в сетевых файловых системах могут быть определены
собственные функции;
□ d_hash(dentry, name) — вычисляет хеш-значение. Это специфичная для
каждой файловой системы хеш-функция, используемая для хеш-таблицы
элементов каталога. Параметр dentry идентифицирует каталог, содержа-
содержащий элемент таблицы. Параметр name указывает на структуру, содержа-
содержащую как путь к элементу, так и значение, возвращаемое хеш-функцией;
□ d_compare(dir, namel, name2) — сравнивает два имени файла. Имя namei
должно содержаться в каталоге, определяемом параметром dir. Функция,
принятая в VFS по умолчанию, выполняет обычное сравнение строк.
Однако в каждой файловой системе этот метод может быть реализован по-
своему. Например, в MS-DOS игнорируется различие между заглавными
и строчными буквами;
□ ddeiete(dentry)— вызывается, когда удаляется последняя ссылка на
объект "элемент каталога" (счетчик обращений dcount становится равным
нулю). Функция, принятая в VFS по умолчанию, не выполняет никаких
действий;
П dreiease (dentry) — вызывается непосредственно перед освобождением
объекта "элемент каталога" (перед тем, как он будет передан slab-
аллокатору). Функция, принятая в VFS по умолчанию, не выполняет ника-
никаких действий;
□ diput (dentry, ino) — вызывается, когда объект "элемент каталога" ста-
становится "отрицательным", т. е. теряет свой индексный дескриптор. Функ-
Функция, принятая в VFS по умолчанию, вызывает iput (), чтобы освободить
объект "индексный дескриптор".
Кэш элементов каталога
Поскольку чтение элемента каталога с диска и создание соответствующего
объекта "элемент каталога" требует значительного времени, имеет смысл
хранить в памяти объекты "элемент каталога", с которыми вы уже не рабо-
работаете, но которые могут понадобиться впоследствии. Например, пользователи
часто редактируют файл, а потом компилируют его, либо редактируют и пе-
печатают файл, либо создают копию файла и редактируют ее. В таких случаях
приходится неоднократно обращаться к одному и тому же файлу.
Чтобы обрабатывать элементы каталога максимально эффективно, в Linux
используется кэш элементов каталога, в котором хранятся структуры двух
типов:
□ набор объектов "элемент каталога", используемых, неиспользуемых или
отрицательных;
□ хеш-таблица для быстрого нахождения объекта "элемент каталога", свя-
связанного с данным именем файла и данным каталогом. Как всегда, если
требуемый объект не находится в кэше элементов каталога, функция по-
поиска возвращает null.
Кэш элементов каталога также служит в качестве управляющего механизма
для кэша индексных дескрипторов. Индексные дескрипторы в памяти ядра,
связанные с неиспользуемыми элементами каталога, не уничтожаются, по-
поскольку кэш элемента каталога "помнит" о них. Таким образом, объекты "ин-
"индексный дескриптор" хранятся в оперативной памяти и могут быть быстро
получены посредством соответствующих объектов "элемент каталога".
Все "неиспользуемые" объекты "элемент каталога" включены в двунаправ-
двунаправленный список "давно не используемых" объектов (список Least Recently
Used, LRU), отсортированный по времени занесения объекта в список. Ины-
Иными словами, объект "элемент каталога", освобожденный последним, ставится
в начало списка, так что объекты, не используемые уже давно, находятся в
конце списка. Когда возникает необходимость уменьшить размер кэша эле-
элементов каталога, ядро удаляет элементы из конца списка, сохраняя объекты,
использованные недавно. Адреса первого и последнего элементов списка
LRU хранятся в полях next и prev переменной dentryunused, имеющей тип
listhead. Поле d_LRU объекта "элемент каталога" содержит указатели на со-
соседние объекты "элемент каталога" в списке.
Каждый "используемый" объект "элемент каталога" заносится в двунаправ-
двунаправленный список, на который указывает поле identry соответствующего объ-
объекта "индексный дескриптор" (список необходим, поскольку каждый индекс-
индексный дескриптор может быть связан с несколькими жесткими ссылками). По-
Поле daiias объекта "элемент каталога" содержит адреса соседних элементов
СПИСКа. Оба ПОЛЯ ИМеЮТ ТИП struct listhead.
"Используемый" объект "элемент каталога" может стать "отрицательным",
когда будет удалена последняя жесткая ссылка на соответствующий файл.
В этом случае объект "элемент каталога" переносится в список LRU. Каждый
раз, когда ядро уменьшает размер кэша элементов каталога, отрицательные
объекты "элемент каталога" перемещаются ближе к концу списка LRU, так
что они постепенно освобождаются (см. разд. "Утилизация страниц сокра-
сокращаемых кэшей диска" в главе 17).
Хеш-таблица реализована в виде массива dentryhashtabie. Каждый его эле-
элемент является указателем на список объектов "элемент каталога", у которых
хеш-значения равны. Размер массива обычно зависит от объема доступной
оперативной памяти. Значением по умолчанию являются 256 элементов на
каждый мегабайт памяти. Поле dhash объекта "элемент каталога" содержит
указатели на соседние элементы списка, связанного с одним хеш-значением.
Хеш-функция вычисляет возвращаемое значение на основании объекта "эле-
"элемент каталога" в каталоге и имени файла.
Спин-блокировка dcacheiock защищает структуры данных кэша элементов
каталога от попыток одновременного обращения к ним в многопроцессорных
системах. Функция diookupo ищет в хеш-таблице родительский объект
"элемент каталога" и имя файла. Во избежание конфликтов она использует
seqlock-блокировку (см. главу 5). Функция diookupo работает аналогично,
но исходит из предположения, что конфликты невозможны, и не применяет
seqlock-блокировку.
Файлы, связанные с процессом
В главе 1 мы говорили, что каждый процесс имеет собственный текущий ра-
рабочий каталог и собственный корневой каталог. Это лишь два примера того,
какие данные должно поддерживать ядро для представления взаимодействия
между процессом и файловой системой. Для этой цели используется структу-
структура данных типа fsstruct (табл. 12.6), а у каждого дескриптора процесса есть
поле f s, которое указывает на структуру f sstruct этого процесса.
Таблица 12.6. Поля структуры fs_struct
Тип Поле Описание
atomic_t count Количество процессов, одновременно использую-
использующих эту таблицу
rwlock_t lock Спин-блокировка чтения/записи для полей таблицы
int umask Битовая маска, используемая при открытии файла
для установки прав доступа
struct dentry * root Элемент корневого каталога
struct dentry * pwd Элемент текущего рабочего каталога
struct dentry * altroot Элемент эмулируемого корневого каталога (всегда
null для архитектуры 80x86)
struct vf smount * rootmnt Объект корневого каталога в смонтированной
файловой системе
struct vf smount * pwdmnt Объект текущего рабочего каталога в смонтирован-
смонтированной файловой системе
struct vf smount * altrootmnt Объект эмулируемого корневого каталога в смонти-
смонтированной файловой системе (всегда null для архи-
архитектуры 80x86)
Вторая таблица, адрес которой содержится в поле files дескриптора процес-
процесса, указывает, какие файлы открыты процессом в данный момент. Это струк-
структура f ilesstruct, поля которой перечислены в табл. 12.7.
Таблица 12.7. Поля структуры files_struct
Тип Поле Описание
atomic_t count Количество процессов, одновременно
использующих эту таблицу
rwlockt f ile_lock Спин-блокировка чтения/записи для поля
таблицы
int max_fds Текущее максимальное количество объектов
int maxfdset Текущее максимальное количество файло-
файловых дескрипторов
int nextfd Значение, на единицу превышающее макси-
максимальное количество когда-либо выделенных
файловых дескрипторов
struct file ** fd Указатель на массив указателей на файло-
файловые объекты
fd_set * close_on_exec Указатель на файловые дескрипторы, кото-
которые должны быть закрыты по exec ()
fd_set * open_fds Указатель на открытые файловые дескрип-
дескрипторы
fd_set close_on_exec_init Первоначальный набор файловых дескрип-
дескрипторов, которые должны быть закрыты по
exec()
fd_set open_fds_init Первоначальный набор файловых дескрип-
дескрипторов
struct file *[] fdarray Первоначальный массив указателей на фай-
файловые объекты
Поле f d указывает на массив указателей на файловые объекты.
Размер этого массива хранится в поле maxfds. Как правило, fd указывает на
поле fdarray структуры f ilesstruct, которое содержит 32 указателя на
файловые объекты. Если процесс откроет более 32 файлов, ядро выделит но-
новый массив (большего размера) под указатели и запишет его адрес в поле fd.
Кроме того, ядро обновит содержимое поля max fds.
Для каждого файла, указатель на который хранится в массиве f d, индекс мас-
массива является дескриптором. Обычно первый элемент (с нулевым индексом)
массива связывается со стандартным потоком ввода процесса, второй со
стандартным потоком вывода, а третий — со стандартным потоком ошибок
(рис. 12.3). Процессы в системе Unix используют дескриптор как основной
идентификатор файла. Обратите внимание, что, благодаря системным вызо-
вызовам dup(), dup2() и fcntio, два дескриптора могут ссылаться на один и тот
же открытый файл, т. е. два элемента массива могут указывать на один и тот
же файловый объект. Пользователи сталкиваются с этим постоянно, когда
применяют конструкции оболочки, такие как 2>&1, для перенаправления со-
сообщений об ошибках в стандартный поток вывода.
Процесс не может использовать более чем nropen файловых дескрипторов
(обычно это значение равно 1 048 576). Кроме того, ядро накладывает дина-
динамическое ограничение на количество файловых дескрипторов в структуре
signai->riim[RLiMiT_NOFiLE] дескриптора процесса. Это значение обычно
равняется 1024, но оно может быть увеличено, если процесс обладает приви-
привилегиями суперпользователя.
Поле openfds первоначально содержит адрес поля openfdsinit, которое
является битовым массивом и идентифицирует дескрипторы файлов, откры-
открытых в данный момент. В поле maxfdset хранится количество битов этого
массива. Поскольку структура данных fdset имеет длину 1024 бита, обычно
нет необходимости в увеличении размера этого битового массива. Однако
ядро может динамически увеличить его размер, если в том возникнет необхо-
необходимости, как в случае с массивом файловых объектов.
Рис. 12.3. Массив fd
Ядро содержит функцию fget (), которая вызывается, когда ядро приступает
к использованию файлового объекта. Эта функция принимает в качестве па-
параметра fd. Она возвращает адрес в current->fiies->fd[fd] (то есть адрес со-
соответствующего файлового объекта) или null, если дескриптору fd не соот-
соответствует никакой файл. В первом случае fgeto увеличивает на единицу
счетчик обращений к файлу fcount.
Ядро также содержит и функцию fput (), вызываемую, когда управляющий
тракт ядра прекращает использование файлового объекта. Функция принима-
ет в качестве параметра адрес файлового объекта и уменьшает на единицу
счетчик обращений к файлу fcount. Более того, если это поле становится
равным нулю, функция вызывает метод release из числа файловых операций
(если таковой определен), уменьшает значение поля iwritecount в объекте
"индексный дескриптор" (если файл был открыт для записи), удаляет файло-
файловый объект из списка суперблока, возвращает файловый объект slab-
аллокатору и уменьшает счетчики обращений связанного объекта "элемент
каталога" и дескриптора файловой системы (см. разд. "Монтирование файло-
файловой системы " далее в этой главе).
Функции fgetiighto и fputiighto являются "ускоренными" версиями
функций f get () и fput (). Ядро вызывает их, когда можно быть уверенным в
том, что текущий процесс уже владеет файловым объектом, т. е. процесс уже
увеличил счетчик ссылок файлового объекта. Например, эти функции ис-
используются служебными процедурами в системных вызовах, которые полу-
получают в качестве аргумента файловый дескриптор, поскольку счетчик ссылок
файлового объекта уже был увеличен предыдущим системным вызовом
open().
Типы файловых систем
Ядро Linux поддерживает много различных типов файловых систем. В сле-
следующих разделах мы опишем несколько файловых систем специальных ти-
типов, которые играют важную роль во внутренней архитектуре ядра Linux.
Затем мы обсудим регистрацию файловой системы— базовую операцию,
которая должна быть выполнена, как правило, на этапе инициализации сис-
системы, до ее первого использования. После того как файловая система зареги-
зарегистрирована, ее специфические функции становятся доступны ядру, и тип
файловой системы может быть смонтирован в ее дереве каталогов.
Специальные файловые системы
В то время как сетевые и дисковые файловые системы позволяют пользова-
пользователю работать с информацией, хранящейся вне ядра, специальные файловые
системы могут предоставить программам и системным администраторам
простой способ манипулирования структурами данных ядра и реализации
особых возможностей операционной системы. В табл. 12.8 перечислены наи-
наиболее распространенные специальные файловые системы, используемые в
Linux. Для каждой из них указана точка монтирования и приведено краткое
описание.
Обратите внимание, что несколько файловых систем не имеют фиксирован-
фиксированной точки монтирования (слово "любая" в соответствующей графе). Пользо-
ватели могут свободно монтировать такие файловые системы и работать с
ними. Кроме того, у некоторых файловых систем точка монтирования и вовсе
отсутствует (слово "нет" в таблице). Они не предназначены для взаимодейст-
взаимодействия с пользователем, но ядро может работать с ними, повторно используя не-
некоторые фрагменты кода слоя VFS. Например, в главе 19 мы убедимся, что,
благодаря специальной файловой системе pipfs, с каналами можно обращать-
обращаться как с FIFO-файлами.
Таблица 12.8. Самые распространенные специальные файловые системы
Название Точка монтирования Описание
bdev Нет Блочные устройства (см. главу 13)
binfnt_misc Любая Различные исполняемые форматы (см. главу 20)
devpts /dev/pts Поддержка псевдотерминала (стандарт Open
Groups Unix 98)
eventpollfs Нет Используется механизмом эффективного опроса
событий
futexfs Нет Используется механизмом futex (Fast Userspace
Locking, быстрая блокировка в пространстве
пользователя)
pipefs Нет Каналы (см. главу 19)
ргос /ргос Общая точка доступа к структурам данных ядра
rootfs Нет Предоставляет пустой корневой каталог на этапе
загрузки
shim Нет Области памяти, совместно используемые при
межпроцессорном взаимодействии (см. главу 19)
mqueue Любая Используется для реализации очередей сообще-
сообщений POSIX (см. главу 19)
sockfs Нет Сокеты
sysfs /sys Общая точка доступа к системным данным
(см. главу 13)
tmpfs Любая Временные файлы (хранятся в оперативной
памяти, если не выполняется подкачка)
usbfs /proc/bus/usb USB-устройства
Специальные файловые системы не привязаны к физическим блочным уст-
устройствам. Однако ядро присваивает каждой смонтированной специальной
файловой системе фиктивное блочное устройство, у которого старший номер
равен нулю, а в качестве младшего номера берется произвольное значение
(индивидуальное у каждой файловой системы). Функция setanonsupero
используется для инициализации суперблоков специальных файловых сис-
систем. В сущности, эта функция получает неиспользуемый младший номер dev
и записывает в поле sdev нового суперблока старший номер, равный нулю, и
младший номер, равный dev. Другая функция, по имени kiiianonsupero,
удаляет суперблок специальной файловой системы. Переменная unnamed_
devidr содержит указатели на вспомогательные структуры, которые отсле-
отслеживают используемые в данный момент младшие номера. Хотя некоторые
разработчики ядер недолюбливают идентификаторы фиктивных блочных
устройств, такое решение позволяет ядру применять унифицированный под-
подход к работе со специальными и обычными файловыми системами.
Далее, в разд. "Монтирование типичной файловой системы ", мы рассмотрим
практический пример того, как ядро определяет и инициализирует специаль-
специальную файловую систему.
Регистрация типа файловой системы
При компиляции ядра пользователь нередко конфигурирует систему Linux
так, чтобы она распознавала все необходимые файловые системы. Однако
код для файловой системы фактически может быть либо включен в образ яд-
ядра, либо динамически загружен в качестве модуля (см. приложение 2). В обя-
обязанности VFS входит отслеживание всех типов файловых систем, код кото-
которых включен в ядро. Для этого существует регистрация типа файловой сис-
системы.
Каждая зарегистрированная файловая система представлена в виде объекта
filesystemtype, поля которого перечислены в табл. 12.9.
Таблица 12.9. Поля объекта f±le_system_type
Тип Поле Описание
const char * name Название файловой системы
int f s_f lags Флаги типа файловой системы
struct super_block *(*)() get_sb Метод для чтения суперблока
void (*) () kill_sb Метод для удаления суперблока
struct module * owner Указатель на модуль, реализующий файло-
файловую систему (см. приложение 2)
struct f ile_system_type * next Указатель на следующий элемент в списке
типов файловых систем
struct list_head f s_supers Голова списка суперблоков с тем же типом
файловой системы
Все объекты, представляющие типы файловых систем, объединены в однона-
однонаправленный список. Переменная filesystems указывает на его первый эле-
элемент, а поле next описанной структуры указывает на следующий элемент
списка. Спин-блокировка чтения/записи fiiesystemiock защищает список
от попыток одновременного обращения.
Поле fssupers является головой (первым пустым элементом) списка супер-
суперблоков, соответствующих смонтированным файловым системам данного ти-
типа. Ссылки на соседние элементы списка хранятся в поле sinstances супер-
суперблока.
Поле getsb указывает на специфичную для файловой системы функцию, ко-
которая создает новый объект-суперблок и инициализирует его (читая инфор-
информацию с диска, если это необходимо). Поле kilisb указывает на функцию,
уничтожающую суперблок.
Поле fsfiags содержит некоторые флаги, перечисленные в табл. 12.10.
Таблица 12.10. Флаги типа файловой системы
Имя Описание
fs_requires_dev Каждая файловая система данного типа должна находиться
на физическом диске
fs_binary_mountdata Файловая система использует двоичную информацию о мон-
монтировании
fsrevaldot Необходимо всегда заново проверять пути "." и ".." в кэше
элементов каталога (для сетевых файловых систем)
fs_odd_rename Операции переименования являются операциями переме-
перемещения (для сетевых файловых систем)
При инициализации системы функция register_fiiesystem() вызывается для
каждой файловой системы, указанной на этапе компиляции. Эта функция за-
заносит соответствующий объект filesystemtype в список типов файловых
систем.
Функция registerfiiesystemo также вызывается при загрузке модуля, реа-
реализующего файловую систему. В этом случае регистрация файловой системы
МОЖет быть аннулирована ВЫЗОВОМ функции unregister_filesystem(), КОГДа
модуль выгружается.
Функция getsbtype (), принимающая название файловой системы в качест-
качестве параметра, просматривает список зарегистрированных файловых систем,
изучая поля name их дескрипторов, и возвращает указатель на соответствую-
соответствующий объект f iiesystemtype, если таковой обнаруживается.
Работа с файловой системой
Как любая традиционная Unix-подобная система, Linux использует корневую
файловую систему. Это файловая система, непосредственно монтируемая
ядром на этапе загрузки компьютера и содержащая сценарии инициализации
системы и наиболее важные системные программы.
Другие файловые системы могут быть смонтированы (либо сценариями ини-
инициализации, либо непосредственно пользователями) в каталогах уже смонти-
смонтированных файловых систем. Будучи деревом каталогов, всякая файловая сис-
система имеет корневой каталог. Каталог, на котором смонтирована файловая
система, называется точкой монтирования. Смонтированная файловая сис-
система является дочерней по отношению к той смонтированной файловой сис-
системе, которой принадлежит каталог-точка монтирования. Например, вирту-
виртуальная файловая система /ргос является дочерней для корневой файловой
системы, а последняя является родительской для /ргос. Корневой каталог
смонтированной файловой системы скрывает содержимое каталога родитель-
родительской файловой системы, являющегося точкой монтирования. Более того, он
скрывает все поддерево родительской файловой системы ниже точки монти-
монтирования.
( Примечание )
Корневой каталог файловой системы может отличаться от корневого каталога
процесса. Как мы видели ранее, корневой каталог процесса — это каталог, со-
соответствующий пути /. По умолчанию корневой каталог процесса совпадает с
корневым каталогом корневой файловой системы операционной системы (точ-
(точнее говоря, с корневым каталогом корневой файловой системы в пространстве
имен процесса, см. следующий раздел), но он может быть изменен с помощью
системного вызова chroot ().
Пространства имен
В традиционной Unix-системе существует только одно дерево смонтирован-
смонтированных файловых систем: начиная с корневой файловой системы операционной
системы, каждый процесс потенциально может обратиться к любому файлу в
смонтированной файловой системе, указав правильный путь. В этом отноше-
отношении система Linux 2.6 устроена сложнее: каждый процесс может иметь соб-
собственное дерево смонтированных файловых систем — так называемое про-
пространство имен процесса.
Как правило, большинство процессов использует одно общее пространство
имен, являющееся деревом смонтированных файловых систем, корень кото-
которого расположен в корневой файловой системе операционной системы, и ис-
используемое процессом init. Однако процесс получает новое пространство
имен, когда он создается системным вызовом clone () с установленным фла-
флагом clonenewns (см. главу 3). Это новое пространство имен наследуется затем
потомками этого процесса, если процесс-родитель создаст их без флага
CLONE_NEWNS.
Когда процесс монтирует (или размонтирует) файловую систему, он лишь
модифицирует свое пространство имен. Следовательно, такое изменение
видно всем процессам, использующим то же пространство имен, причем
только им. Более того, процесс может изменить корневую файловую систему
своего пространства имен с помощью системного вызова pivotroot, специ-
специфичного для Linux.
Пространство имен процесса представлено структурой namespace, на которую
указывает поле namespace дескриптора процесса. Поля структуры namespace
представлены в табл. 12.11.
Таблица 12.11. Поля структуры namespace
Тип Поле Описание
atomic_t count Счетчик обращений (показывает, сколько процессов
совместно используют данное пространство имен)
struct vf smount * root Дескриптор смонтированной файловой системы для
корневого каталога пространства имен
struct listhead list Голова списка дескрипторов всех смонтированных
файловых систем
struct rw_semaphore sem Семафор чтения/записи, защищающий эту структуру
Поле list является головой циклического двунаправленного списка, в кото-
котором перечислены все смонтированные файловые системы, принадлежащие
данному пространству имен. Поле root задает смонтированную файловую
систему, которая представляет корень дерева смонтированных файловых сис-
систем этого пространства имен. Как мы увидим в следующем разделе, смонти-
смонтированные файловые системы представлены структурами vf smount.
Монтирование файловых систем
В большинстве традиционных ядер Unix-подобных операционных систем
каждая файловая система может быть смонтирована только один раз. Пред-
Предположим, что файловая система Ext2, хранящаяся на дискете /dev/fdO, монти-
монтируется на /flp при помощи команды:
mount -t ext2 /dev/fdO /flp
Пока эта файловая система не будет размонтирована командой umount, любая
попытка монтирования, использующая /dev/fdO, закончится неудачно.
В Linux все не так. Здесь можно многократно монтировать одну и ту же фай-
файловую систему. Конечно, если файловая система смонтирована п раз, то к ее
корневому каталогу можно обратиться через п точек монтирования, по одной
на каждую операцию. Хотя конкретная файловая система доступна через раз-
разные точки монтирования, в действительности она уникальна. Таким образом,
для них всех существует только один объект-суперблок, независимо от того,
сколько раз файловая система была смонтирована.
Смонтированные файловые системы образуют иерархическую структуру:
точка монтирования одной файловой системы может быть каталогом другой
файловой системы, которая, в свою очередь, смонтирована на третьей, и т. д.
( Примечание )
Удивительно, но точка монтирования файловой системы может быть каталогом
этой же файловой системы, если она уже была смонтирована. Например:
mount -t ext2 /dev/fdO /flp; touch /flp/foo
mkdir /flp/mnt; mount -t ext2 /dev/fdO /flp/mnt
Теперь к пустому файлу foo, принадлежащему файловой системе с дискеты,
можно обращаться и как /flp/foo, и как /flp/mnt/foo.
Кроме того, можно "наслаивать" несколько смонтированнных файловых сис-
систем на одну точку монтирования. Каждая новая файловая система, смонтиро-
смонтированная на этой точке, скрывает предыдущую, хотя процессы, уже исполь-
использующие файлы и каталоги прежней файловой системы, могут продолжать это
делать. Когда самая верхняя файловая система размонтируется, та, что была
под ней, снова становится видна.
Нетрудно догадаться, что отслеживание смонтированных файловых систем
может быстро превратиться в сплошной кошмар. Для каждой операции мон-
монтирования ядро должно хранить в памяти точку монтирования и флаги, а
также информацию о том, как соотносится монтируемая файловая система
с уже смонтированными. Все эти данные находятся в дескрипторе смонтиро-
смонтированной файловой системы, имеющем тип vfsmount. Поля этого дескриптора
приведены в табл. 12.12.
Таблица 12.12. Поля структуры vfsmount
Тип Поле Описание
struct list_head mnt_hash Указатели для списка хеш-таблицы
struct vfsmount * mntparent Указывает на родительскую файловую
систему, на которой смонтирована данная
Таблица 12.12 (окончание)
Тип Поле Описание
struct dentry * mnt_mountpoint Указывает на элемент каталога, являюще-
являющегося точкой монтирования этой файловой
системы
struct dentry * mnt_root Указывает на элемент корневого каталога
этой файловой системы
struct super_block * mnt_sb Указывает на суперблок этой файловой
системы
struct list_head mnt_mounts Голова списка дескрипторов всех файло-
файловых систем, смонтированных на каталогах
этой файловой системы
struct list_head mnt_child Указатели для списка mnt_mounts, пере-
перечисляющего дескрипторы смонтированных
файловых систем
atomicjt mntcount Счетчик обращений (увеличивается для
запрета размонтирования файловой
системы)
int mnt_flags Флаги
int mnt_expiry_mark Флаг, устанавливаемый в значение
ИСТИНА, если у файловой системы истек
срок существования (она может быть
автоматически размонтирована, если
этот флаг установлен, и никто ее не
использует)
char * mnt_devname Имя файла-устройства
struct list_head mnt_list Указатели на список дескрипторов смонти-
смонтированных файловых систем, принадлежа-
принадлежащих данному пространству имен
struct list_head mnt_f slink Указатели на список со сроками существо-
существования, специфичный для файловой
системы
struct namespace * mnt_namespace Указатель на пространство имен процесса,
смонтировавшего эту файловую систему
Структуры vf smount хранятся в нескольких циклических двунаправленных
связных списках:
□ хеш-таблица, индексируемая по адресу дескриптора vfsmount родитель-
родительской файловой системы и адресу объекта "элемент каталога" каталога, яв-
являющегося точкой монтирования. Эта хеш-таблица хранится в массиве
mounthashtabie, размер которого зависит от объема доступной оператив-
ной памяти. Каждый элемент таблицы является головой циклического
двунаправленного связного списка дескрипторов, имеющих одно и то же
хеш-значение. Поле mnthash дескриптора содержит указатели на соседние
элементы этого списка;
□ циклический двунаправленный список всех дескрипторов смонтирован-
смонтированных файловых систем, принадлежащих конкретному пространству имен
(создается для каждого пространства имен). Поле list структуры
namespace содержит голову этого списка, а поле mntiist дескриптора
vf smount содержит указатели на соседние элементы списка;
□ циклический двунаправленный список всех дочерних смонтированных
файловых систем (для каждой смонтированной файловой системы). Голо-
Голова каждого списка хранится в поле mntmounts дескриптора смонтирован-
смонтированной файловой системы, а поле mntchild дескриптора содержит указатели
на соседние элементы списка.
Спин-блокировка vfsmntiock защищает список объектов смонтированных
файловых систем от попыток одновременного обращения.
Поле mntf lags дескриптора содержит значение, образованное флагами, оп-
определяющими, как следует обрабатывать некоторые типы файлов в смонти-
смонтированной файловой системе. Эти флаги, которые, кстати, можно установить
с помощью опций команды mount, перечислены в табл. 12.13.
Таблица 12.13. Флаги смонтированной файловой системы
Имя Описание
mnt_nosuid Запретить флаги setuid и setgid в смонтированной файловой системе
mnt_nodev Запретить доступ к файлам устройств в смонтированной файловой сис-
системе
mnt_noexec Запретить выполнение программ в смонтированной файловой системе
Приведем несколько функций, обрабатывающих дескрипторы смонтирован-
смонтированных файловых систем:
□ aiiocvfsmnt (name)— выделяет и инициализирует дескриптор смонтиро-
смонтированной файловой системы;
□ freevfsmnt(mnt)— освобождает дескриптор смонтированной файловой
системы, на который указывает mnt;
□ iookup_mnt(mnt, dentry) — ищет дескриптор в хеш-таблице и возвращает
его адрес.
Монтирование типичной файловой системы
Сейчас мы опишем действия, выполняемые ядром при монтировании файло-
файловой системы. Мы начнем с рассмотрения файловой системы, монтируемой на
каталоге уже смонтированной файловой системы. (При этом новую файло-
файловую систему мы будем называть "типичной".)
Для монтирования типичной файловой системы применяется системный вы-
вызов mount (). Его служебная процедура sysmount () воздействует на следую-
следующие параметры:
□ путь к файлу устройства, содержащему файловую систему, или null, если
в этом пути нет необходимости (например, когда монтируется сетевая
файловая система);
□ путь к каталогу, на котором будет смонтирована файловая система (к точ-
точке монтирования);
□ тип файловой системы. Это должно быть название зарегистрированной
файловой системы;
□ флаги монтирования (допустимые значения указаны в табл. 12.14);
□ указатель на структуру, специфичную для файловой системы (который
может иметь значение null).
Таблица 12.14. Флаги, используемые системным вызовом mount ()
Флаг Описание
ms_rdonly Файлы доступны только для чтения
MS_NOSUID Запретить флаги setuid и setgid
ms_nodev Запретить доступ к файлам устройств
ms_noexec Запретить выполнение программ
ms_synchronous Операции записи в файлы и каталоги выполняются немедленно
ms_remount Заново смонтировать файловую систему с изменением флагов мон-
монтирования
ms_mandlock Обязательная блокировка разрешена
ms_dirsync Операции записи в каталоги выполняются немедленно
ms_noatime He обновлять время обращения к файлу
ms_nodiratime He обновлять время обращения к каталогу
ms_bind Создать связанное монтирование, что позволит сделать файл или
каталог видимым из другой точки системного дерева каталогов
(опция —bind команды mount)
Таблица 12.14 (продолжение)
Флаг Описание
ms_move Атомарно переместить смонтированную файловую систему
на другую точку монтирования (опция —move команды mount)
msrec Рекурсивно создать связанное монтирование для поддерева
каталога
ms_verbose Генерировать сообщения ядра об ошибках монтирования
Функция sysmount() копирует значения параметров во временные буфе-
буферы ядра, включает глобальную блокировку ядра и вызывает функцию
domount (). Когда domount () возвращает управление, служебная процедура
снимает глобальную блокировку и освобождает временные буферы ядра.
Функция domount () несет ответственность за собственно монтирование, вы-
выполняя следующие операции:
1. Если установлены хотя бы некоторые из флагов msnosuid, msnodev или
msnoexec, она сбрасывает их и устанавливает соответствующие флаги
(mnt_nosuid, mnt_nodev, mntjnoexec) в объекте, представляющем монтируе-
монтируемую файловую систему.
2. Анализирует путь к точке монтирования, вызывая функцию path lookup (),
которая сохраняет результат в локальной переменной nd, имеющей тип
nameidata (см. разд. "Анализ пути" далее в этой главе).
3. Изучает флаги монтирования, чтобы определить свои дальнейшие дейст-
действия. В частности:
• если флаг msremount установлен, это обычно означает намерение изме-
изменить флаги монтирования в поле s_f lags суперблока и флаги монти-
монтируемой файловой системы в поле mntf lags объекта, представляющего
монтируемую файловую систему. Эти изменения выполняет функция
do_remount () ;
• в противном случае она проверяет флаг msbind. Если он установлен,
значит, пользователь просит сделать файл или каталог видимым в дру-
другой точке системного дерева каталогов;
• в противном случае она проверяет флаг msmove. Если он установлен,
значит, пользователь просит изменить точку монтирования уже смон-
смонтированной файловой системы. Функция domovemount () делает это ав-
автоматически;
• в противном случае она вызывает функцию donewmount (). Это самый
распространенный случай. Он имеет место, когда пользователь хочет
смонтировать либо какую-то специальную файловую систему, либо
обычную, хранящуюся в разделе диска. Функция donewmount () вызы-
вызывает функцию dokernmount (), передавая ей тип файловой системы,
флаги монтирования и имя блочного устройства. Эта функция, отве-
отвечающая за операцию монтирования и возвращающая адрес дескриптора
новой смонтированной файловой системы, описана в следующем раз-
разделе. Затем функция do_new_mount () ВЫЗЫВает функцию do_add_mount (),
которая выполняет следующие действия:
а получает семафор namespace->sem текущего процесса для записи, по-
поскольку эта функция собирается модифицировать пространство
имен;
а функция dokernmount о могла приостановить процесс, а тем вре-
временем другой процесс мог смонтировать файловую систему на на-
нашей точке монтирования или даже изменить нашу корневую файло-
файловую систему (current->namespace->root). Поэтому обсуждаемая
функция убеждается, что файловая система, смонтированная по-
последней на этой точке монтирования, все еще указывает пространст-
пространство имен процесса current. Если это не так, функция освобождает се-
семафор чтения/записи и возвращает код ошибки;
п если монтируемая файловая система уже смонтирована на точке
монтирования, переданной системному вызову в качестве парамет-
параметра, или если точка монтирования является символьной ссылкой,
функция освобождает семафор чтения/записи и возвращает код
ошибки;
п инициализирует флаги в поле mntf lags объекта, представляющего
монтируемую файловую систему и созданного функцией
do kern mount();
D вызывает функцию grafttreeo, чтобы занести объект, представ-
представляющий монтируемую файловую систему, в список имен, хеш-
таблицу и список потомков родительской файловой системы;
п освобождает семафор чтения/записи namespace->sem и возвращает
управление.
4. Вызывает функцию pathreiease () для завершения анализа пути к точке
монтирования и возвращает 0.
Функция do_kern_mount()
Центральной частью операции монтирования является функция do_kern_
mount (), которая проверяет флаги типа файловой системы, чтобы определить,
как должно проходить монтирование. Эта функция принимает следующие
параметры:
□ f stype — название файловой системы, которая должна быть смонтирована;
□ flags — флаги монтирования (см. табл. 12.14);
□ name — путь к блочному устройству, на котором хранится файловая сис-
система (либо тип файловой системы, если она специальная);
□ data — указатель на дополнительные данные, которые должны быть пере-
переданы методу readsuper файловой системы.
Эта функция фактически выполняет операцию монтирования, производя сле-
следующие действия:
1. Вызывает функцию getf stype () для просмотра списка типов файловых
систем и поиска названия, переданного в параметре fstype. Функция
getf stype () возвращает адрес соответствующего дескриптора f iie_
systemtype в локальной переменной type.
2. Вызывает функцию aiiocvfsmnto для выделения дескриптора монтируе-
монтируемой файловой системы и сохраняет его адрес в локальной переменной mnt.
3. Вызывает функцию type->get_sb (), специфичную для файловой системы,
для выделения нового суперблока и его инициализации.
4. Инициализирует поле mnt->mnt_sb адресом нового суперблока.
5. Инициализирует поле mnt->mnt_root адресом объекта "элемент каталога",
соответствующего корневому каталогу файловой системы, и увеличивает
счетчик обращений к объекту "элемент каталога".
6. Инициализирует поле mnt->mnt_parent значением переменной mnt (в слу-
случае типичной файловой системы необходимое значение поля mntparent
устанавливается, когда дескриптор монтируемой файловой системы зано-
заносится в соответствующие списки функцией graft_tree).
7. Инициализирует поле mnt->mnt_namespace значением из current->
namespace.
8. Освобождает семафор чтения/записи smount суперблока (семафор был
получен при выделении объекта на шаге 3).
9. Возвращает адрес mnt объекта, представляющего смонтированную файло-
файловую систему.
Выделение суперблока
Метод getsb объекта, представляющего файловую систему, обычно реализу-
реализуется функцией, состоящей из одной строки кода. Например, в файловой сис-
системе Ext2 этот метод реализован так:
struct super_block * ext2_get_sb(struct file_system_type *type,
int flags, const char *dev_name,
void *data)
{
return get sb_bdev(type, flags, dev_name, data, ext2_fill super);
}
Функция виртуальной файловой системы getsbbdev () выделяет и инициа-
инициализирует новый суперблок, подходящий для дисковых файловых систем. Она
принимает адрес функции ext2_fiii_super(), которая считывает дисковый
суперблок из раздела Ext2 на диске.
Для выделения суперблоков, подходящих для специальных файловых систем,
виртуальная файловая система предоставляет следующие функции:
getsbpseudo () (для специальных файловых систем без точки монтирования,
например, pipfs), getsbsingie о (для специальных файловых систем с един-
единственной точкой монтирования, например, sysfs) и getsbnodev () (для спе-
специальных файловых систем, которые могут быть монтированы неоднократно,
например, tmpfs).
Приведем самые важные операции из числа выполняемых функцией
get_sb_bdev ()'.
1. Вызывает openbdevexci (), чтобы открыть блочное устройство с именем
devname (см. разд. "Драйверы символьных устройств" в главе 13).
2. Вызывает sgeto для осуществления поиска в списке суперблоков файло-
файловой системы (type->fs_supers, см. разд. "Регистрация типа файловой
системы"ранее в этой главе). Если суперблок, относящийся к блочному
устройству, уже присутствует, функция возвращает его адрес. В против-
противном случае она выделяет и инициализирует новый объект-суперблок, за-
заносит его в список файловой системы и в глобальный список суперблоков,
а затем возвращает его адрес.
3. Если суперблок не является новым (то есть не был выделен на предыду-
предыдущем шаге, потому что файловая система уже была смонтирована), функ-
функция переходит к шагу 6.
4. Копирует значение параметра flags в поле s_flags суперблока и заносит в
ПОЛЯ sid, soldblocksize И sblocksize Значения, соответствующие
блочному устройству.
5. Вызывает специфичную для файловой системы функцию, переданную в
качестве последнего аргумента функции getsbbdev (), чтобы прочитать
данные суперблока с диска и заполнить другие поля нового суперблока.
6. Возвращает адрес нового объекта-суперблока.
Монтирование корневой файловой системы
Монтирование корневой файловой системы является важнейшей частью
инициализации системы. Это довольно сложная процедура, потому что ядро
Linux позволяет корневой файловой системе находиться в разных местах, на-
например, в разделе жесткого диска, на дискете, в удаленной файловой системе,
доступной через NFS, или даже на ramdisk (являющемся фиктивным блочным
устройством, хранящимся в оперативной памяти).
Для простоты описания предположим, что корневая файловая система хра-
хранится в разделе жесткого диска (в конце концов, это самый распространен-
распространенный случай). Когда загружается операционная система, ядро находит в пере-
переменной rootdev старший номер диска, который содержит корневую файло-
файловую систему (см. приложение 1). Корневая файловая система может быть
указана как файл устройства в каталоге /dev либо при компиляции ядра, либо
путем передачи соответствующей опции "root" начальному загрузчику. Ана-
Аналогичным образом флаги монтирования корневой файловой системы хранят-
хранятся в переменной rootmount flags. Пользователь указывает эти флаги либо при
помощи внешней программы rdev в откомпилированном образе ядра, либо
передавая соответствующую опцию rootfiags начальному загрузчику (см.
приложение 1).
Монтирование корневой файловой системы состоит из двух этапов.
□ Ядро монтирует специальную файловую систему rootfs, которая просто
предоставляет пустой каталог в качестве первоначальной точки монтиро-
монтирования.
□ Ядро монтирует реальную файловую систему на пустом каталоге.
Почему ядро берет на себя труд монтирования файловой системы rootfs до
монтирования реальной? Дело в том, что файловая система rootfs позволяет
ему легко заменять реальную корневую файловую систему. В действительно-
действительности, ядро иногда монтирует и размонтирует несколько файловых систем, од-
одну за другой. Например, загрузчик, находящийся на CD-ROM с дистрибути-
дистрибутивом, может загрузить в оперативную память ядро с минимальным набором
драйверов, которое смонтирует в качестве корневой минимальную файловую
систему, хранящуюся в ramdisk. Затем программы из этой первоначальной
корневой файловой системы "прозондируют" доступное оборудование (на-
(например, определят тип жесткого диска EIDE, SCSI или какой-то еще), загру-
загрузят все необходимые модули ядра и смонтируют новую файловую систему
с физического блочного устройства.
Этап 1. Монтирование файловой системы rootfs
Первый этап выполняется функциями initrootf s () и init_mount_tree(), ко-
которые вызываются во время инициализации системы.
Функция initrootfso регистрирует тип специальной файловой системы
rootfs:
struct file_system_type rootfs_fs_type = {
.name = "rootfs";
.get_sb = rootfs_get_sb:
.kill_sb = kill_litter_super;
}
register_filesystem(rootfs_fs_type);
Функция init mounttree () выполняет следующие операции:
1. Вызывает функцию dokernmount (), передавая ей строку rootfs в качест-
качестве типа файловой системы, и сохраняет адрес дескриптора смонтирован-
смонтированной файловой системы, возвращенный этой функцией в локальной пере-
переменной mnt. Как было сказано в предыдущем разделе, функция
dokernmounto, в конечном счете, вызывает метод getsb файловой сис-
системы rootfs, Т. е. функцию rootfs_get_sb() I
struct superblock *rootfs_get_sb(struct file_system_type *fs type,
int flags, const char *dev_name, void *data)
{
return get_sb_nodev(fs_type, flags|MS_NOUSER, data,
ramfs_fill_super) ;
}
Функция getsb nodev (), в свою очередь, выполняет следующие шаги:
• вызывает sget () для выделения нового суперблока, передавая в качест-
качестве параметра адрес функции setanonsuper () (см. разд. "Специальные
файловые системы" ранее в этой главе). В результате поле sdev су-
суперблока устанавливается должным образом: старший номер равен ну-
нулю, младший номер выбирается отличным от младших номеров других
смонтированных специальных файловых систем;
• копирует значение параметра flags в поле s_flags суперблока;
• вызывает ramfsfiiisuper для выделения объекта "индексный деск-
дескриптор" и соответствующего объекта "элемент каталога" и заполнения
полей суперблока. Поскольку rootfs является специальной файловой
системой и не имеет дискового суперблока, необходимо реализовать
лишь пару операций суперблока;
• возвращает новый суперблок.
2. Выделяет объект namespace для пространства имен процесса 0 и заносит
в него дескриптор смонтированной файловой системы, возвращенный
функцией do_kern_mount ():
namespace = kmalloc(sizeof(*namespace), GFP_KERNEL);
list_add(&mnt->mnt_list, &namespace->list);
namespace->root = mnt;
mnt->mnt_namespace = init_t ask. namespace = namespace;
3. Записывает в поле namespace всех прочих процессов в системе адрес объ-
объекта-пространства имен, а также инициализирует счетчик обращений
namespace->count. (По умолчанию все процессы совместно используют од-
одно исходное пространство имен.)
4. Устанавливает в качестве корневого каталога и текущего каталога процес-
процесса 0 корневую файловую систему.
Этап 2. Монтирование реальной корневой
файловой системы
Второй этап операции монтирования корневой файловой системы выполня-
выполняется ядром ближе к концу инициализации системы. Существует несколько
способов смонтировать реальную корневую файловую систему в соответст-
соответствии с опциями, выбранными при компиляции ядра и опциями первоначаль-
первоначальной загрузки, переданными загрузчику ядра. Ради краткости мы рассмотрим
случай дисковой файловой системы, чье имя устройства было передано ядру
через загрузочный параметр "root". Мы будем предполагать, что, кроме фай-
файловой системы rootfs, никакая другая первоначальная файловая система не
используется.
Функция preparenamespace () выполняет следующие операции:
1. Записывает в переменную rootdevicename имя файла устройства, полу-
полученное из загрузочного параметра "root" . Кроме того, она записывает в
переменную rootdev старший и младший номера этого файла устройства.
2. Вызывает функцию mountroot (), которая, в свою очередь:
• ВЫЗЫВаеТ Служебную Процедуру sysjnknod () СИСТеМНОГО ВЫЗОВа mknod ()
для создания файла устройства /dev/root в первоначальной корневой
файловой системе rootfs с теми же старшим и младшим номерами, ко-
которые хранятся в переменной rootdev;
• выделяет буфер и заносит в него список названий файловых систем.
Этот список был либо передан ядру в параметре загрузки "rootfstype",
либо построен путем сканирования элементов однонаправленного спи-
списка типов файловых систем;
• сканирует список названий файловых систем, построенный на преды-
предыдущем шаге. Для каждого названия обсуждаемая функция вызывает
функцию sysmount () и пытается смонтировать файловую систему со-
ответствующего типа на корневом устройстве. Поскольку в каждом ме-
методе, специфичном для файловой системы, используется свое магиче-
магическое число, все обращения к getsb () закончатся неудачно, за исклю-
исключением того, в котором будет сделана попытка заполнить суперблок с
помощью функции файловой системы, реально используемой на кор-
корневом устройстве. Файловая система монтируется в каталоге /root фай-
файловой системы rootfs;
• вызывает syschdir ("/root") для изменения корневого каталога про-
процесса.
3. Переносит точку монтирования новой файловой системы в корневой ката-
каталог файловой системы rootfs:
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
Обратите внимание, что специальная файловая система не размонтируется;
она всего лишь скрывается под корневой дисковой файловой системой.
Размонтирование файловой системы
Системный вызов umount () служит для размонтирования файловой системы.
Соответствующая служебная процедура sysumount () работает с двумя пара-
параметрами: с именем файла (это либо каталог, являющийся точкой монтирова-
монтирования, либо имя файла блочного устройства) и набором флагов. Она выполняет
следующие действия:
1. Вызывает функцию pathiookup () для анализа пути к точке монтирования.
Эта функция возвращает результаты анализа через локальную переменную
nd, имеющую тип nameidata (см. следующий раздел).
2. Если полученный каталог не является точкой монтирования какой-либо
файловой системы, обсуждаемая функция устанавливает в переменной
retval код возврата -einval и переходит к шагу 6. Эта проверка выполня-
выполняется путем выяснения, содержит ли nd->mnt->mnt_root адрес объекта "эле-
"элемент каталога", на который указывает nd.dentry.
3. Если файловая система, подлежащая размонтированию, не была смонти-
смонтирована в данном пространстве имен, функция устанавливает в переменной
retval код возврата -einval и переходит к шагу 6. (Вспомним, что у неко-
некоторых специальных файловых систем нет точки монтирования.) Чтобы
ВЫПОЛНИТЬ ЭТу Проверку, ВЫЗЫВаеТСЯ фуНКЦИЯ checkjnnt () ДЛЯ nd->mnt.
4. Если у пользователя нет привилегий, требуемых для размонтирования
файловой системы, обсуждаемая функция устанавливает в переменной
retval код возврата -ЕРЕРМ и переходит к шагу 6.
5. Вызывает функцию doumount (), передавая ей в качестве параметров объ-
объект, представляющий смонтированную файловую систему (nd.mnt) и набор
флагов (flags). Эта функция выполняет следующие операции:
• извлекает адрес суперблока sb из поля mntsb объекта, представляюще-
представляющего смонтированную файловую систему;
• если пользователь запросил форсированное размонтирование, функция
прерывает любую текущую операцию монтирования при помощи опе-
операции суперблока umount_begin;
• если файловая система, подлежащая размонтированию, является кор-
корневой, и пользователь не требует отключения файловой системы, эта
функция вызывает doremount (), чтобы заново смонтировать корневую
файловую систему только для чтения, и возвращает управление;
• получает для записи семафор namespace->sem текущего процесса и
СПИН-блОКИрОВКу vf smountlock;
• если смонтированная файловая система не включает в себя точек мон-
монтирования дочерних смонтированных файловых систем, или если поль-
пользователь запросил форсированное отключение файловой системы,
функция вызывает umounttreeO для размонтирования файловой сис-
системы вместе со всеми дочерними системами;
• освобождает СПИН-блОКИрОВКу vf smountlock И семафор namespace->sem
текущего процесса.
6. Уменьшает счетчики обращений объекта "элемент каталога1', соответст-
соответствующего корневому каталогу файловой системы, и дескриптора смонти-
смонтированной файловой системы. (Эти счетчики были увеличены функцией
path_lookup ().)
7. Возвращает retvai.
Анализ пути
Когда некоторый процесс должен работать с файлом, он передает путь к это-
этому файлу соответствующему системному вызову VFS, например, open о,
mkdir (), rename () или stat (). В этом разделе мы покажем, как VFS выполняет
анализ пути, т. е. получает индексный дескриптор по заданному пути к
файлу.
Стандартная процедура, выполняющая эту задачу, состоит из просмотра пути
и разбиения его на последовательность имен. Все имена, кроме последнего,
должны идентифицировать каталоги.
Если путь начинается с "/", значит, он является абсолютным, и поиск начина-
начинается с каталога, идентифицируемого с помощью current->fs->root (корневой
каталог процесса). В противном случае путь является относительным, и по-
поиск начинается с каталога, идентифицируемого с помощью current->fs->pwd
(текущего каталога процесса).
Получив элемент каталога, а следовательно, и индексный дескриптор исход-
исходного каталога, код изучает элемент пути, соответствующий первому имени,
чтобы вычислить нужный индексный дескриптор. Затем файл каталога, кото-
который имеет этот индексный дескриптор, считывается с диска, и элемент пути,
соответствующий второму имени, изучается с целью вычисления соответст-
соответствующего индексного дескриптора. Процедура повторяется для каждого име-
имени, входящего в путь.
Кэш элементов каталога значительно ускоряет эту процедуру, потому что
объекты "элемент каталога", использованные недавно, еще хранятся в памя-
памяти. Как мы видели ранее, каждый такой объект связывает имя файла в кон-
конкретном каталоге с соответствующим индексным дескриптором. Следова-
Следовательно, во многих случаях при анализе имени файла удается избежать чтения
промежуточных каталогов с диска.
Однако, не все так просто, потому что необходимо учитывать следующие
особенности файловых систем Unix и VFS:
□ следует проверять права доступа к каждому каталогу, чтобы убедиться,
что данному процессу разрешено чтение содержимого этого каталога;
□ имя файла может оказаться символьной ссылкой, соответствующей произ-
произвольному пути. В этом случае приходится проводить анализ всех элемен-
элементов этого пути;
□ символьные ссылки могут образовать замкнутый круг. Ядро должно учи-
учитывать такую возможность и прерывать бесконечный цикл, если он воз-
возникнет;
□ имя файла может быть точкой монтирования смонтированной файловой
системы. Эта ситуация должна быть распознана, а операцию анализа пути
будет необходимо продолжить в новой файловой системе;
П анализ пути должен происходить в пределах пространства имен процесса,
выполнившего системный вызов. Один и тот же путь, используемый раз-
разными процессами с разными пространствами имен, может указывать на
различные файлы.
Анализ пути выполняется функцией pathiookup (), которая принимает три
параметра:
□ name — указатель на путь, подлежащий анализу;
□ nd — адрес структуры nameidata, в которой хранятся результаты анализа.
Ее поля приведены в табл. 12.15;
□ flags — комбинация значений флагов, которая определяет, как будет про-
происходить обращение к искомому файлу. Допустимые значения перечисле-
перечислены в табл. 12.16.
Когда функция pathiookup () возвращает управление, структура nameidata, на
которую указывает переменная nd, уже заполнена данными, имеющими от-
отношение к операции анализа пути.
Таблица 12.15. Поля структуры nameidata
Тип Поле Описание
struct dentry * dentry Адрес объекта "элемент каталога"
struct vfsjnount * mnt Адрес объекта, представляющего смонтирован-
смонтированную файловую систему
struct qstr last Последний элемент пути (используется, когда
установлен флаг lookup_parent)
unsigned int flags Флаги анализа
int lasttype Тип последнего элемента пути (используется,
когда установлен флаг lookup_parent)
unsigned int depth Текущий уровень вложенности символьной ссыл-
ссылки (см. ниже); это значение должно быть меньше 6
char [ ] * savednames Массив путей, связанных с вложенными символь-
символьными ссылками
union intent Объединение из одного элемента, указывающее
способ доступа к файлу
Поля dentry и mnt указывают, соответственно, на объект "элемент каталога" и
объект, представляющий смонтированную файловую систему, которые соот-
соответствуют последнему найденному элементу пути. Эти два поля определяют
файл, идентифицируемый данным путем.
Поскольку объект "элемент каталога" и объект, представляющий смонтиро-
смонтированную файловую систему, возвращенные функцией pathiookup () в струк-
структуре nameidata, являются результатом анализа пути, их нельзя освобождать,
пока процесс, вызвавший функцию pathiookup (), не закончит пользоваться
ими. Поэтому pathiookup () увеличивает счетчики обращений у обоих объек-
объектов. Если вызвавший процесс захочет освободить эти объекты, он вызовет
функцию pathreiease (), передав ей в качестве параметра адрес структуры
nameidata.
Поле flags содержит комбинацию значений некоторых флагов, используе-
используемых в операции анализа пути. Эти флаги перечислены в табл. 12.16. Боль-
шинство из них может быть передано вызывающим процессом функции
path_lookup () через параметр flags.
Таблица 12.16. Флаги операции анализа пути
Флаг Описание
lookup_follow Если последний элемент является символьной ссылкой, интерпре-
интерпретировать ее (следовать по ней)
lookup_directory Последний элемент должен быть каталогом
lookup_continue В пути еще остались имена, которые необходимо разобрать
lookup_parent Просмотреть каталог, который содержит последний элемент пути
lookupnoalt Не принимать во внимание эмулируемый корневой каталог (флаг
бесполезен в архитектуре 80x86)
lookupjdpen Намерение открыть файл
lookup_create Намерение создать файл (если таковой не существует)
lookup_access Намерение проверить права пользователя на доступ к файлу
Функция pathiookup () выполняет следующие действия:
1. Инициализирует некоторые поля параметра nd следующим образом:
• устанавливает поле lasttype в значение lastroot (это необходимо,
если путь представляет собой косую черту или последовательность
косых черт; см. разд. "Анализ родительского каталога" далее в этой
главе);
• устанавливает поле flags в значение параметра flags;
• устанавливает поле depth в ноль.
2. Получает для чтения семафор current->f s->iock текущего процесса.
3. Если первым символом пути является косая черта (/), анализ должен начи-
начинаться с корневого каталога процесса current. Функция получает адрес
объекта, представляющего соответствующую смонтированную файловую
систему (current->fs->rootrmt), и адрес объекта "элемент каталога"
(current->f s->root). Затем она увеличивает счетчики обращений этих объ-
объектов и сохраняет адреса в nd->mnt и nd->dentry соответственно.
4. В противном случае, если первым символом пути является не косая черта,
операция анализа должна начаться с текущего рабочего каталога процесса
current. Функция получает адрес объекта, представляющего соответст-
соответствующую смонтированную файловую систему (current->f s->pwdmnt), и ад-
адрес объекта "элемент каталога" (current->f s->pwd). Затем она увеличивает
счетчики обращений этих объектов и сохраняет адреса в nd->mnt и nd->
dentrу соответственно.
5. Освобождает семафор current->f s->iock текущего процесса.
6. Обнуляет поле totaiiinkcount дескриптора текущего процесса (см.
разд. "Анализ символьных ссылок" далее в этой главе).
7. Вызывает функцию linkpathwaiko, выполняющую собственно опера-
операцию анализа:
return link_path_walk(name, nd);
Теперь мы готовы описать главную операцию анализа пути, а именно функ-
функцию linkpathwalk (). В качестве параметров она получает указатель name на
анализируемый путь И адрес nd Структуры nameidata.
Чтобы немного упростить изложение, мы вначале опишем действия функции
iink_path_waik() в том случае, когда флаг lookup_parent не установлен, и
путь не содержит символьных ссылок (ситуация стандартного анализа пути).
Затем мы обсудим случай, в котором флаг lookupparent установлен: этот вид
анализа производится при создании, удалении или переименовании записи в
каталоге, т. е. при анализе пути к родительскому каталогу. Наконец, мы объ-
объясним, как эта функция разрешает символьные ссылки.
Стандартный анализ пути
Когда флаг lookup_parent сброшен, функция iink_path_waik() выполняет
следующие действия:
1. Инициализирует локальную переменную lookupf lags значением nd->f lags.
2. Пропускает все косые черты (/), предшествующие первому элементу
пути.
3. Если оставшаяся часть пути пуста, возвращает ноль. В структуре
nameidata ПОЛЯ dentry И rant указывают на объекты, ОТНОСЯЩИеСЯ К ПОСЛед-
нему обработанному элементу оригинального пути.
4. Если значение в поле depth дескриптора nd положительно, функция уста-
устанавливает флаг lookupfollow в локальной переменной lookupf lags (см.
разд. "Анализ символьных ссылок").
5. Выполняет цикл, в котором путь, переданный в параметре name, разбивает-
разбивается на элементы (косые черты интерпретируются как разделители имен
файлов).
6. Для каждого выделенного элемента эта функция извлекает адрес объекта
"индексный дескриптор" последнего обнаруженного элемента из nd->
dentry->d_inode. На первом проходе цикла индексный дескриптор отно-
относится к каталогу, с которого должен начаться анализ пути.
7. Убеждается, что у последнего найденного элемента, хранящегося в ин-
индексном дескрипторе, разрешено выполнение (в Unix можно просмотреть
записи каталога только в том случае, если он помечен как исполняемый).
Если индексный дескриптор имеет собственный метод permission, функ-
функция выполняет его; в противном случае она вызывает функцию
execpermissioniiteO, которая проверяет права доступа, хранящиеся в
поле imode объекта "индексный дескриптор", и привилегии текущего
процесса. В обоих случаях, если последний выделенный элемент не раз-
разрешает выполнение, функция linkpathwaiko прерывает цикл и возвра-
возвращает сообщение об ошибке.
8. Рассматривает следующий найденный элемент пути. По имени элемента
функция вычисляет 32-битовое хеш-значение, которое будет использова-
использовано при просмотре хеш-таблицы кэша элементов каталога.
9. Если за косой чертой, завершающей имя анализируемого элемента пути,
непосредственно следует какое-то количество косых черт, игнорирует их
все.
10. Если анализируемый элемент является последним в исходном пути, пере-
переходит к шагу 6.
И. Если в качестве элемента пути указана одиночная точка ("."), функция
переходит к следующему элементу. Точка указывает на текущий каталог
и не имеет никакого эффекта внутри пути.
12. Если в качестве элемента пути указаны две точки ("..")? функция пытается
перейти в родительский каталог:
• если последний обработанный каталог в пути является корневым ката-
каталогом процесса (nd->dentry равно current->fs->root, a nd->mnt равно
current->fs->rootmnt), то переход вверх по дереву каталогов не разре-
разрешен. Функция вызывает foiiowmount о для последнего проанализиро-
проанализированного элемента пути и переходит к следующему элементу;
• если последний обработанный каталог в пути является корневым ката-
каталогом фаЙЛОВОЙ Системы nd->rant (nd->dentry равно nd->mnt->mnt_root),
а файловая система nd->mnt не смонтирована поверх другой файловой
системы (nd->mnt равно nd->mnt->mnt_parent), то файловая система
nd->mnt обычно3 является корневой системой пространства имен. В та-
таком случае переход вверх по дереву невозможен, и функция вызывает
3 Эта ситуация может также возникнуть для сетевых файловых систем, отсоединенных от дерева
каталогов пространства имен.
foiiowmounto для последнего проанализированного элемента пути и
переходит к следующему элементу;
• если последний обработанный каталог в пути является корневым ката-
каталогом файловой системы nd->mnt, и она смонтирована поверх другой
файловой системы, требуется переключение файловых систем. Обсуж-
Обсуждаемая фуНКЦИЯ Записывает В nd->dentry Значение В nd->mnt->mnt_
mountpoint, а в nd->mnt значение nd->mnt->mnt_parent, после чего пере-
ходит к шагу 20 (вспомним, что на одной точке монтирования может
быть смонтировано несколько файловых систем);
• если последний обработанный каталог в пути не является корневым ка-
каталогом смонтированной файловой системы, то функция должна про-
просто подняться в родительский каталог. Она записывает в nd->dentry
значение nd->dentry->d_parent, вызывает foiiowmounto для родитель-
родительского каталога и переходит к следующему элементу.
Функция foiiowmounto проверяет, является ли nd->dentry точкой мон-
монтирования какой-нибудь фаЙЛОВОЙ Системы (nd->dentry->d_mounted
больше нуля). В таком случае она вызывает lookupmnt () для поиска кор-
корневого каталога смонтированной файловой системы в кэше элементов ка-
каталога и обновляет nd->dentry и nd->mnt адресами объектов, соответст-
соответствующих смонтированной файловой системе; затем она повторяет всю
операцию (на одной точке монтирования может быть смонтировано не-
несколько фаЙЛОВЫХ СИСТем). В СУЩНОСТИ, ВЫЗОВ фуНКЦИИ follow_mountО
при переходе вверх к родительскому каталогу необходим, потому что
процесс мог начать анализ пути с каталога, входящего в файловую систе-
систему, скрытую другой, смонтированной на родительском каталоге.
13. Имя элемента— не "." и не "..". В этом случае функция должна искать
его в кэше элементов каталога. Если файловая система предоставляет
собственный метод dhash элемента каталога, функция вызывает его для
изменения хеш-значения, вычисленного ранее на шаге 8.
14. Устанавливает флаг lookupcontinue в поле nd->fiags, чтобы обозначить
присутствие следующего элемента, подлежащего анализу.
15. Вызывает функцию doiookupo, чтобы вычислить объект "элемент ката-
каталога", связанный с данным родительским каталогом (nd->dentry) и име-
именем файла (обрабатываемым элементом пути). Вызванная функция вна-
вначале вызывает diookup () для поиска объекта "элемент каталога", при-
принадлежащего элементу пути, в кэше элементов каталога. Если такого
объекта нет, функция doiookupo вызывает reaiiookup (). Последняя
считывает каталог с диска, вызывая метод lookup индексного дескрипто-
дескриптора, создает новый объект "элемент каталога" и заносит его в кэш элемен-
тов каталога. Затем она создает новый объект "индексный дескриптор" и
вставляет его в кэш индексных дескрипторов.
( Примечание )
В некоторых случаях эта функция может сразу найти требуемый индексный де-
дескриптор в кэше индексных дескрипторов. Это бывает, когда элемент является
последним в пути и не ссылается на каталог, у соответствующего файла есть
несколько жестких ссылок, и наконец, к файлу недавно обращались по жесткой
ссылке, отличной от той, что используется в данном пути.
В конце данного шага поля dentry и mnt локальной переменной next бу-
будут, соответственно, указывать на объект "элемент каталога" и объект,
представляющий смонтированную файловую систему, соответствующие
элементу пути, обрабатываемому в этом цикле.
16. Вызывает функцию foiiowmounto, чтобы проверить, ссылается ли толь-
только что обработанный элемент пути (next.dentry) на каталог, являющийся
ТОЧКОЙ МОНТИрОВаНИЯ КаКОЙ-НИбуДЬ фаЙЛОВОЙ СИСТеМЫ (next.dentry->
djnounted больше нуля). Функция follow_mount () обновляет next.dentry И
next.mnt так, что они указывают на объект "элемент каталога" и объект,
представляющий самую "верхнюю" файловую систему, смонтированную
на каталоге, соответствующем данному элементу пути (см. шаг 12).
17. Проверяет, ссылается ли только что обработанный элемент пути на сим-
символьную ссылку (next, dentry- >d_inode имеет собственный метод
f oiiowiink). Мы обсудим этот случай далее в разд. "Анализ символьных
ссылок".
18. Проверяет, ссылается ли только что обработанный элемент пути на ката-
каталог (next.dentry->d_inode имеет собственный метод lookup). Если это не
так, функция возвращает код ошибки -enotdir, потому что данный эле-
элемент находится где-то в середине оригинального пути.
19. Заносит в nd->dentry значение next.dentry, а в nd->mnt— значение
next.mnt, после чего переходит к следующему элементу пути.
20. Итак, обработаны все элементы оригинального пути, кроме последнего.
Функция сбрасывает флаг lookupcontinue в переменной nd->f lags.
21. Если путь заканчивается косой чертой, функция устанавливает флаги
LOOKUP_FOLLOW И LOOKUP_DIRECTORY В ЛОКаЛЬНОЙ Переменной lookup_flags,
чтобы заставить последующие функции интерпретировать последний
элемент как имя каталога.
22. Проверяет значение флага lookupparent в переменной lookupfiags.
В дальнейшем мы будем предполагать, что флаг сброшен в ноль.
23. Если в качестве последнего элемента пути указана одиночная точка ("."),
функция заканчивает выполнение и возвращает ноль (нет ошибок).
В структуре nameidata, на которую указывает nd, поля dentry и mnt ссы-
ссылаются на объекты, относящиеся к предпоследнему элементу пути (лю-
(любой элемент "." внутри пути не имеет никакого эффекта).
24. Если в качестве последнего элемента пути указаны две точки (".."), функ-
функция пытается перейти в родительский каталог:
• если последний обработанный каталог в пути является корневым ката-
каталогом процесса (nd->dentry равно current->fs->root, a nd->mnt равно
current->fs->rootmnt), функция вызывает foiiow_mounto для предпо-
следнего элемента пути и заканчивает выполнение, возвращая ноль
(нет ошибки). Поля dentry и mnt ссылаются на объекты, относящиеся к
предпоследнему элементу пути, т. е. на корневой каталог процесса;
• если последний обработанный каталог в пути является корневым ката-
каталогом фаЙЛОВОЙ Системы nd->mnt (nd->dentry paBHO nd->mnt->
mntroot), и файловая система nd->mnt не смонтирована поверх другой
фаЙЛОВОЙ Системы (nd->mnt равно nd->mnt->mnt_parent), TO переход
вверх по дереву невозможен, и функция вызывает foiiowmount о для
предпоследнего элемента пути и заканчивает выполнение, возвращая
ноль (нет ошибки);
• если последний обработанный каталог в пути является корневым ката-
каталогом файловой системы nd->mnt, и она смонтирована поверх другой
файловой системы, функция записывает в nd->dentry значение
nd->mnt->mnt_mountpoint, а В nd->mnt значение nd->mnt->mnt_parent, ПО-
сле чего возобновляет выполнение шага 24;
• если последний обработанный каталог в пути не является корневым
каталогом смонтированной файловой системы, то функция записывает
В nd->dentry Значение nd->dentry->d_parent, вызывает followjnount ()
для родительского каталога и заканчивает выполнение, возвращая
ноль (нет ошибки). Поля nd->dentry и nd->mnt ссылаются на объекты,
относящиеся к элементу, который предшествует предпоследнему эле-
элементу пути.
25. Имя элемента— не "." и не "..". В этом случае функция должна искать
его в кэше элементов каталога. Если файловая система реализует собст-
собственный метод dhash элемента каталога, функция вызывает его для изме-
изменения хеш-значения, вычисленного ранее на шаге 8.
26. Вызывает функцию doiookup (), чтобы вычислить объект "элемент ката-
каталога", связанный с данным родительским каталогом и именем файла (см.
шаг 15). В конце данного шага локальная переменная next содержит ука-
затели на объект "элемент каталога" и дескриптор смонтированной фай-
файловой системы, относящиеся к последнему элементу пути.
27. Вызывает функцию foiiowmountо, чтобы проверить, является ли по-
последний элемент пути точкой монтирования какой-нибудь файловой сис-
системы, и, если это так, обновить локальную переменную next, записав в
нее адреса объекта "элемент каталога" и объекта, представляющего смон-
смонтированную файловую систему, которые относятся к корневому каталогу
самой "верхней" смонтированной файловой системы.
28. Проверяет, установлен ли флаг lookupfollow в переменной lookupf lags,
и есть ли у объекта "индексный дескриптор" next.dentry->d_inode собст-
собственный метод foiiowiink. Если это так, значит, элемент является сим-
символьной ссылкой, которую следует интерпретировать, как описано в
разд. "Анализ символьных ссылок".
29. Элемент не является символьной ссылкой, или данную символьную
ссылку не нужно интерпретировать. Тогда функция записывает в поля
nd->mnt И nd->dentry значения, хранящиеся В next.mnt И next.dentry COOT-
ветственно. Последний объект "элемент каталога" является результатом
всей операции анализа.
30. Проверяет nd->dentry->d_inode на равенство null. Это имеет место, когда
нет индексного дескриптора, связанного с объектом "элемент каталога",
как правило, из-за того, что путь ссылается на несуществующий файл.
В таком случае функция возвращает код ошибки -enoent.
31. Индексный дескриптор, связанный с последним элементом пути, сущест-
существует. ЕСЛИ флаг LOOKUPDIRECTORY установлен В переменной lookupf lags,
функция проверяет, есть ли у индексного дескриптора собственный ме-
метод (то есть является ли этот элемент пути каталогом). Если нет, то функ-
функция возвращает код ошибки -enotdir.
32. Возвращает ноль (нет ошибок). Значения полей nd->dentry и nd->mnt со-
соответствуют последнему элементу пути.
Анализ пути к родительскому каталогу
Во многих случаях реальной целью операции анализа является не последний,
а предпоследний элемент пути. Например, при создании файла последний
элемент является именем еще не существующего файла, а предыдущая часть
пути указывает на каталог, в котором должна быть создана новая ссылка.
Следовательно, операция анализа пути должна возвратить объект "элемент
каталога" предпоследнего компонента. Другой пример: удаление файла,
идентифицируемого путем /foo/bar, состоит в удалении bar из каталога foo.
Таким образом, ядру в действительности интересен каталог foo, а не bar.
Флаг lookupparent используется, когда операция анализа должна найти ка-
каталог, содержащий последний элемент пути, а не сам этот элемент.
Когда флаг lookupparent установлен, функция linkpathwaiko устанавли-
устанавливает также ПОЛЯ last И last_type структуры данных nameidata. Поле last СО-
держит имя последнего элемента пути. Поле lasttype идентифицирует тип
последнего элемента. Оно принимает одно из значений, представленных
в табл. 12.17.
Таблица 12.17. Значения поля last_type структуры данных nameidata
Значение Описание
last_norm Последний элемент является именем обычного файла
last_root Последний элемент является символом 7" (то есть весь путь — У)
last_dot Последний элемент является символом "."
lastjdotdot Последний элемент является символом ".."
last_bind Последний элемент является символьной ссылкой на специальную
файловую систему
Флаг lastroot устанавливается функцией pathiookup () по умолчанию при
запуске операции анализа пути (описание в начале разд. "Анализ пути"). Если
оказывается, что путь состоит лишь из косой черты, ядро не изменяет значе-
значение ПОЛЯ lasttype.
Остальные значения поля lasttype устанавливаются функцией
iink_path_waik(), когда установлен флаг lookup_parent. В этом случае функ-
функция выполняет шаги, описанные в предыдущем разделе, вплоть до шага 22.
Последующая операция обработки последнего элемента пути продолжается
по-другому:
1. Функция записывает в поле nd->iast имя последнего элемента.
2. Инициализирует поле nd->iast_type значением lastnorm.
3. Если последний компонент представлен одиночной точкой ("."), функция
записывает в поле nd->iast_type значение lastdot.
4. Если последний компонент представлен двумя точками (".."), функция за-
записывает в поле nd->iast_type значение last_dotdot.
5. Возвращает значение ноль (нет ошибок).
Нетрудно заметить, что последний элемент не интерпретируется вовсе. Та-
Таким образом, когда функция возвращает управление, поля dentry и mnt струк-
структуры nameidata указывают на объекты, относящиеся к каталогу, который
включает в себя последний элемент пути.
Анализ символьных ссылок
Вспомним, что символьная ссылка — это обычный файл, в котором находит-
находится путь к другому файлу. Любой путь может включать в себя символьные
ссылки, и ядро должно их обрабатывать.
Если, например, /foo/bar является символьной ссылкой, указывающей на ../dir
(то есть содержащей этот путь), то путь /foo/bar/file должен быть интерпрети-
интерпретирован ядром как ссылка на файл dir/file/. В этом примере ядро выполнит две
операции анализа. Первая обработает /foo/bar. Когда ядро обнаружит, что bar
является именем символьной ссылки, оно должно будет прочитать ее содер-
содержимое и интерпретировать его как самостоятельный путь. Вторая операция
анализа пути начнется с каталога, до которого дошла первая операция, и про-
продолжится, пока не будет определен последний элемент пути в символьной
ссылке. Затем первая операция анализа возобновляется с элемента каталога,
достигнутого второй операцией, с компонентом, следующим за символьной
ссылкой в оригинальном пути.
Ситуацию осложняет то обстоятельство, что путь, включенный в символьную
ссылку, может содержать другие символьные ссылки. Вы, возможно, поду-
подумали, что код ядра, разрешающий символьные ссылки, сложен для понима-
понимания, но это не так. На самом деле, код вполне прост, потому что рекурсивен.
Однако неконтролируемая рекурсия потенциально опасна. Например, пред-
предположим, что символьная ссылка указывает сама на себя. Естественно, ана-
анализ пути, содержащего такую символьную ссылку, может породить беско-
бесконечный поток рекурсивных вызовов, что быстро приведет к переполнению
стека ядра. Поле linkcount дескриптора текущего процесса служит для пре-
предотвращения этой проблемы. Оно увеличивается перед каждым рекурсивным
вызовом и уменьшается сразу после него. Если будет предпринята попытка
шестой вложенной операции анализа, вся операция прерывается с кодом
ошибки. Таким образом, уровень вложенности символьных ссылок не может
быть больше 5.
Более того, поле totaiiinkcount дескриптора текущего процесса отслежи-
отслеживает количество символьных ссылок (даже невложенных), по которым про-
проследовала операция анализа оригинального пути. Если этот счетчик дойдет
до 40, операция анализа прерывается. Без такого счетчика злонамеренный
пользователь смог бы создать патологический путь, содержащий много по-
последовательных символьных ссылок, и "подвесить" работу ядра с помощью
очень длинной операции анализа пути.
В принципе, код работает следующим образом: после того, как функция
linkpathwaiko прочитает объект "элемент каталога", связанный с компо-
компонентом пути, она проверит, есть ли у соответствующего индексного дескрип-
тора собственный метод foiiowiink (см. разд. "Стандартный анализ пути").
Если это так, значит, индексный дескриптор является символьной ссылкой,
которая должна быть интерпретирована прежде, чем операция анализа ори-
оригинального пути будет продолжена.
В ЭТОМ Случае функция link_path_walk() вызывает функцию do_follow_
linko, передавая ей адрес dentry объекта "элемент каталога", представляю-
представляющего символьную ссылку, и адрес nd структуры nameidata. В свою очередь,
функция dof oiiowiink () выполняет следующие действия:
1. Убеждается, что current->iink_count меньше 5. В противном случае воз-
возвращает КОД ОШИбкИ -ELOOP.
2. Убеждается, что current->totai_iink_count меньше 40. В противном слу-
случае возвращает код ошибки -eloop.
3. Вызывает condresched () для переключения процессов, если этого требу-
требует Текущий Процесс (флаг TIFJNEEDJRESCHED В Дескрипторе thread_info
текущего процесса установлен).
4. Увеличивает счетчики current->link_count, current->total_link_count И
nd->depth.
5. Обновляет время обращения к объекту "индексный дескриптор", связан-
связанному с символьной ссылкой, требующей интерпретации.
6. Вызывает специфичную для файловой системы функцию, реализующую
метод foiiowiink, передавая ей параметры dentry и nd. Эта функция из-
извлекает путь, содержащийся в индексном дескрипторе символьной ссыл-
ссылки, и сохраняет этот путь в соответствующем элементе массива nd->
saved_names.
7. Вызывает функцию vfs foiiowiinko, передавая ей адрес nd и адрес
пути В массиве nd->saved_names.
8. Если метод putiink объекта "индексный дескриптор" определен, выпол-
выполняет его, освобождая таким образом временные структуры данных, выде-
выделенные МеТОДОМ f ollowlink.
9. Уменьшает значения ПОЛеЙ current->link_count И nd->depth.
10. Возвращает код ошибки, возвращенный функцией vfs foiiowiinko
(ноль, если ошибок не было).
Со своей стороны, функция vfs foiiowiinko выполняет следующие дей-
действия:
1. Проверяет, является ли косой чертой первый символ пути, содержащегося
в символьной ссылке. Если это так, значит, обнаружен абсолютный путь, и
нет необходимости держать в памяти информацию о предыдущем пути.
Тогда функция вызывает pathreiease () для структуры nameidata, тем са-
мым освобождая объекты, полученные на предыдущих шагах анализа. За-
Затем она устанавливает поля dentry и mnt структуры данных так, чтобы они
соответствовали корневому каталогу текущего процесса.
2. Вызывает функцию linkpathwaiko для обработки пути символьной
ссылки, передавая путь и nd в качестве параметров.
3. Возвращает значение, полученное от linkpathwalk ().
Когда dofoiiowiinko заканчивает выполнение, в поле dentry локальной
переменной next находится адрес объекта "элемент каталога", на который
указывает символьная ссылка. Это сделано специально для вызвавшей функ-
функции linkpathwalk (), которая теперь может переходить к следующему шагу.
Реализация системных вызовов VFS
Недостаток места не позволяет нам обсудить реализацию всех системных вы-
вызовов, перечисленных в табл. 12.1. Однако будет полезно кратко изложить
реализацию нескольких системных вызовов, чтобы проиллюстрировать взаи-
взаимодействие структур данных VFS.
Вернемся к примеру, рассмотренному в начале главы. Пользователь выдает
команду оболочки, копирующую MS-DOS-файл /floppy/TEST в Ех12-файл
/tmp/test. Оболочка вызывает внешнюю программу, например, ср, которая,
предположим, выполнит следующий фрагмент кода:
inf - open("/floppy/TEST", 0-RDONLY, 0);
outf = open("/tmp/test", 0_WRONLY | O_CREAT | O_TRUNC, 0600);
do {
len = read(inf, buf, 4096);
write(outf, buf, len);
}while (len);
close(outf);
close(inf);
На самом деле, код реальной программы ср сложнее, поскольку она должна
еще проверять коды ошибок, возможно, возвращенных системным вызовом.
В нашем примере мы сосредоточим внимание на "нормальном" поведении
операции копирования.
Системный вызов ореп()
Системный вызов open о обслуживается функцией sysopeno, которая при-
принимает в качестве параметров filename (путь к файлу, который нужно от-
крыть), flags (флаги режима доступа) и mode (битовую маску прав доступа),
если требуется создать файл. В случае успеха системный вызов возвращает
дескриптор файла, т. е. индекс, присвоенный новому файлу в массиве указа-
указателей на файловые объекты current->fiies->fd; иначе возвращается -1.
В рассматриваемом примере open () вызывается дважды. Первый раз, чтобы
открыть /floppy/TEST для чтения (флаг ordonly), а второй — чтобы открыть
/tmp/test для записи (флаг owronly). Если файл /tmp/test еще не существует,
он создается (флаг ocreat) с исключительными правами владельца на чтение
и запись (восьмеричное число Обоо в качестве третьего параметра).
В противном случае, если файл существует, он перезаписывается (флаг
otrunc). Все флаги системного вызова open о перечислены в табл. 12.18.
Таблица 12.18. Флаги системного вызова open ()
Имя флага Описание
o_rdonly Открыть для чтения
O_WRONLY ОТКРЫТЬ ДЛЯ ЗЭ ПИ СИ
o_rdwr Открыть для чтения и записи
o_creat Создать файл, если он не существует
o_excl Совместно с o_creat: возвратить ошибку, если файл существует
o_noctty He считать файл управляющим терминалом
otrunc Очистить файл (удалить текущее содержимое)
o_append Всегда писать в конец файла
o_nonblock Никакие системные вызовы для этого файла не будут блокирующими
O_NDELAY ТО же, ЧТО O_NONBLOCK
osync Синхронная запись (блокировать, пока не завершится физическая опе-
операция записи)
fasync Уведомление о вводе/выводе с помощью сигналов
odirect Прямая передача данных ввода/вывода (без буферизации ядром)
O_LARGEFILE БОЛЬШОЙ фаЙЛ (больше 2 Гбайт)
odirectory Завершить аварийно, если файл не является каталогом
o_nofollow He следовать по завершающей символьной ссылке в пути
onoatime Не обновлять время последнего обращения к индексному дескриптору
Опишем работу функции sysopen (). Она выполняет следующие шаги:
1. Вызывает getname (), чтобы прочитать из адресного пространства процесса
путь к файлу.
2. Вызывает get_unused_fd(), чтобы наЙТИ незанятый СЛОТ В current->
fiies->fd. Соответствующий индекс (дескриптор нового файла) хранится
в локальной переменной f d.
3. Вызывает функцию filpopeno, передавая ей в качестве параметров путь,
флаги режима доступа и битовую маску прав доступа. В свою очередь, эта
функция выполняет следующие шаги:
• копирует флаги режима доступа в namei_f lags, но кодирует флаги ре-
режима доступа ordonly, owronly и ordwr специальным образом: нуле-
нулевой (самый младший) бит nameifiags устанавливается, только если
доступ к файлу требует прав на чтение, а бит с номером 1 устанавлива-
устанавливается, только если доступ к файлу требует прав на запись. Обратите
внимание, что в системном вызове open () невозможно указать, что дос-
доступ к файлу не требует привилегий на чтение или запись. Однако это
имеет смысл при анализе пути, содержащем символьные ссылки;
• вызывает функцию open namei (), передавая ей в качестве параметров
путь, модифицированные флаги режима доступа и адрес локальной
структуры nameidata. Эта функция анализирует путь следующим об-
образом:
D если флаг ocreat не установлен среди флагов режима доступа,
функция запускает операцию анализа пути со сброшенным флагом
lookup_parent и установленным флагом lookup_open. Кроме того,
флаг lookup_follow устанавливается, только если флаг o_nofollow
сброшен, а флаг lookupdirectory устанавливается, только если флаг
o_directory установлен;
D если среди флагов режима доступа флаг ocreat установлен, функ-
функция запускает операцию анализа с установленными флагами
LOOKUP_PARENT, LOOKUP_OPEN И LOOKUP_CREATE. ЕСЛИ фуНКЦИЯ
pathiookup() завершит свое выполнение успешно, обсуждаемая
функция проверит, существует ли запрошенный файл. В противном
случае она выделит новый дисковый индексный дескриптор, вызвав
метод create родительского индексного дескриптора;
( Примечание ^
Функция opennamei (), помимо прочего, выполняет несколько проверок на
безопасность в отношении файла, который определила операция анализа пути.
В частности, проверяется, существует ли индексный дескриптор, связанный с
найденным объектом "элемент каталога", является ли он обычным файлом, и
разрешен ли текущему процессу доступ к этому файлу в соответствии с флага-
флагами режима доступа. Кроме того, если файл открывается для записи, функция
убеждается, что он не заблокирован другими процессами.
• вызывает функцию dentryopen (), передавая ей адреса объекта "эле-
"элемент каталога" и объекта, представляющего смонтированную систему,
которые были получены в результате анализа пути. Вызванная функция
выполняет следующее:
D выделяет новый файловый объект;
п инициализирует поля f_f lags и fmode файлового объекта в соответ-
соответствии с флагами режима доступа, переданными системному вызову
open();
п инициализирует поля fdentry и f_vf smnt файлового объекта в соот-
соответствии с адресами объекта "элемент каталога" и объекта, пред-
представляющего смонтированную файловую систему, переданными в
качестве параметров;
п записывает в поле fop содержимое поля iop соответствующего
объекта "индексный дескриптор". Это действие устанавливает все
методы, необходимые для последующих файловых операций;
D заносит файловый объект в список открытых файлов, на который
указывает поле s_f iles суперблока файловой системы;
а если метод open определен среди файловых операций, функция вы-
вызывает его;
° вызывает fiierastateinit о для инициализации структур данных
опережающего чтения (см. главу 16);
D если флаг odirect установлен, функция проверяет, можно ли вы-
выполнить операции прямого ввода/вывода на этом файле (см. гла-
главу 16);
и возвращает адрес файлового объекта;
• возвращает адрес файлового объекта.
4. Записывает в current->fiies->fd[fd] адрес файлового объекта, возвра-
возвращенный функцией dentryopen ().
5. Возвращает fd.
Системные вызовы readQ и writeQ
Вернемся к коду программы ср. Системный вызов open () возвращает два де-
дескриптора файла, которые сохраняются в переменных inf и outf. Далее про-
программа входит в цикл, на каждом шаге которого очередная порция файла
/floppy/TEST копируется в локальный буфер (системный вызов read ()), а за-
затем данные из локального буфера записываются в файл /tmp/test (системный
ВЫЗОВ write ()).
Системные вызовы read () и write () во многом схожи. Оба принимают три
параметра: дескриптор файла fd? адрес buf некоторой области памяти (буфе-
(буфера, содержащего пересылаемые данные) и число count, указывающее, сколько
байтов должно быть передано. Естественно, read о пересылает данные из
файла в буфер, a write о — в обратном направлении. Оба системных вызова
возвращают либо количество успешно переданных байтов, либо -1, что явля-
является индикатором ошибки.
Если возвращено значение, меньшее, чем count, это не означает, что про-
произошла ошибка. Ядро имеет право в любой момент прервать системный вы-
вызов, даже если не все запрошенные байты были получены. Пользовательское
приложение должно проверять возвращаемое значение и повторять систем-
системный вызов, если это необходимо. Как правило, небольшое значение возвра-
возвращается, когда выполняется чтение из канала или терминального устройства,
когда достигнут конец файла или когда системный вызов прерван сигналом.
Состояние "конец файла" (EOF) легко распознается по нулевому значению,
возвращенному системным вызовом read (). Это состояние не следует путать
с аварийным завершением по сигналу. Если read () прерывается сигналом до
чтения данных, возникнет ошибка.
Операция чтения или записи всегда выполняется с точки, определяемой фай-
файловым указателем (полем fpos файлового объекта). Оба системных вызова
обновляют файловый указатель, увеличивая его на количество переданных
байтов.
Опуская подробности, можно сказать, что sysreado (служебная процедура
системного вызова read о) и sys_write() (служебная процедура системного
вызова write о) выполняют практически одни и те же действия. Каждая
из них:
1. Вызывает функцию fgetiight о, чтобы по дескриптору fd вычислить ад-
адрес file соответствующего файлового объекта (см. ранее разд. "Файлы,
связанные с процессом").
2. Если флаги в fiie->f_mode запрещают доступ запрошенного типа (опера-
(операцию записи или чтения), служебная процедура возвращает код ошибки
-EBADF.
3. Если у объекта file нет файловой операции read о или aioreado (соот-
(соответственно, write () ИЛИ aiowrite () ), ВОЗВращаеТСЯ КОД Ошибки -EINVAL.
4. Вызывает функцию accessoko для грубой проверки параметров buf и
count (см. главу 10).
5. Вызывает функцию rwverifyareao, чтобы проверить, нет ли конфлик-
конфликтующих обязательных блокировок для соответствующей порции файла.
Если это так, служебная процедура возвращает код ошибки или приоста-
навливает текущий процесс, если блокировка была установлена с по-
помощью команды fsetlkw (см. разд. "Блокировка файлов" далее в этой
главе).
6. Вызывает метод f ile->f_op->read (соответственно, f ile->f_op->write) ДЛЯ
передачи данных, если он определен; в противном случае вызывает метод
file->f_op->aio_read (file->f_op->aio_write). Все ЭТИ методы, обсуждае-
мые в главе 16, возвращают фактически переданное количество байтов.
В качестве побочного эффекта обновляется файловый указатель.
7. Вызывает fput light (), чтобы освободить файловый объект.
8. Возвращает фактически переданное количество байтов.
Системный вызов closeQ
Цикл в нашем примере заканчивается, когда системный вызов read () возвра-
возвращает 0, т. е. когда все байты файла /floppy/TEST скопированы в файл
/tmp/test. После этого программа может закрывать открытые файлы, посколь-
поскольку операция копирования завершена.
Системный вызов close о принимает в качестве параметра дескриптор fd
файла, который должен быть закрыт. Служебная процедура sysciose () вы-
выполняет следующие действия:
1. Получает адрес файлового объекта, сохраненный в current->fiies->
fd[fd]. Если он равен null, возвращает код ошибки.
2. Устанавливает current->fiies->fd[fd] в значение null. Освобождает фай-
файловый дескриптор fd, сбрасывая соответствующие биты в полях openfds
И close_on_exec структуры current->f iles (см. главу 20).
3. Вызывает функцию f iipciose (), которая делает следующее:
• вызывает метод flush из числа файловых операций, если он определен;
• снимает все обязательные блокировки с файла, если таковые были
установлены (см. следующий раздел);
• вызывает fput () для освобождения файлового объекта.
4. Возвращает 0 или код ошибки. Код ошибки мог быть получен от метода
flush или вызван ошибкой в предшествовавшей операции записи в файл.
Блокировка файлов
Когда к файлу могут обратиться несколько процессов, возникает проблема
синхронизации. Что произойдет, если два процесса попытаются записать
данные в одно и то же место файла? Что произойдет, если один процесс чита-
читает данные из какого-то места в файле, а другой пишет туда?
В традиционных Unix-подобных системах одновременное обращение к од-
одному месту в файле ведет к непредсказуемым результатам. Однако системы
Unix обладают механизмом, позволяющим процессам заблокировать участок
файла так, что одновременных обращений удастся избежать.
В стандарте POSIX предусмотрен механизм блокировки файлов, основанный
на системном вызове fcnti (). Можно заблокировать произвольный участок
файла (хоть один байт) или весь файл (включая данные, которые будут до-
добавлены в него впоследствии). Поскольку процесс может заблокировать
фрагмент файла, он может наложить несколько блокировок на разные участ-
участки файла.
Такой тип блокировки не является препятствием для процесса, ничего не
знающего о блокировании. Подобно семафору, служащему для защиты кри-
критической области кода, такая блокировка считается рекомендательной, по-
поскольку она не действует, если какой-то процесс отказывается от сотрудни-
сотрудничества и не проверяет наличие блокировки перед обращением к файлу. По-
Поэтому блокировки POSIX называются рекомендательными блокировками.
Традиционные варианты BSD реализуют рекомендательное блокирование с
помощью системного вызова flock (). Этот вызов не позволяет процессу за-
заблокировать отдельный участок файла — только файл целиком. В традици-
традиционных вариантах System V есть библиотечная функция lockf (), которая явля-
является всего лишь интерфейсом к f cnti ().
Более важен тот факт, что в System V Release 3 появилось обязательное бло-
блокирование. Ядро следит за тем, чтобы при любой попытке сделать системный
вызов open (), read () и write () не нарушалась обязательная блокировка соот-
соответствующего файла. Таким образом, обязательные блокировки действуют
даже для процессов, не сотрудничающих друг с другом.
( Примечание )
Удивительно, но процесс может удалить файл, даже если какой-то другой про-
процесс установил на нем обязательную блокировку! Эта парадоксальная ситуа-
ситуация возможна, потому что процесс, удаляющий файловую жесткую ссылку, не
модифицирует содержимое файла, а меняет только содержимое родительского
каталога.
Независимо от того, пользуются ли процессы рекомендательными или обяза-
обязательными блокировками, они могут использовать совместные блокировки на
чтение и исключительные блокировки на запись. Несколько процессов могут
иметь блокировку на чтение какого-то участка файла, но только у одного
процесса может быть блокировка на запись в этот участок в данный момент
времени. Более того, невозможно получить блокировку на запись, когда дру-
другой процесс обладает блокировкой на чтение того же участка файла, и наобо-
наоборот.
Блокировка файлов в Linux
Linux поддерживает все типы блокирования: обязательные и рекомендатель-
рекомендательные блокировки, а также системные вызовы fcntio и flock о (lockf () реали-
реализован в виде стандартной библиотечной функции).
Ожидаемое поведение системного вызова flock о в любой Unix-подобной
операционной системе заключается в постановке только рекомендательных
блокировок, независимо от состояния флага монтирования msmandlock. Од-
Однако в Linux есть специальный вид обязательной блокировки, реализуемой в
flock о, которая используется для поддержки ряда проприетарных сетевых
файловых систем. Речь идет о так называемой обязательной блокировке в
совместном режиме. Когда она установлена, никакому другому процессу не
разрешено открывать файл, если это приведет к конфликту с режимом досту-
доступа, определенным блокировкой. Не следует пользоваться этой возможностью
в приложениях, ориентированных на традиционные Unix-системы, поскольку
исходный код не будет переносимым.
В Linux был введен еще один вид блокировки, основанный на fcntio и на-
названный арендой. Когда процесс пытается открыть файл, защищенный бло-
блокировкой "аренда", он блокируется, как обычно. Однако процесс, владеющий
блокировкой, получает сигнал. Будучи проинформированным, он должен
вначале обновить файл, чтобы его содержимое было непротиворечивым, а
затем — снять блокировку. Если он не сделает этого в течение строго опре-
определенного времени (указанного в файле /proc/sys/fs/lease-break-time в секун-
секундах; как правило, 45 секунд), блокировка автоматически снимается ядром, и
заблокированный процесс может продолжать работу.
Процесс может получить или освободить рекомендательную блокировку
файла двумя способами:
□ выполнив системный вызов flock (). Параметрами этого вызова являются
дескриптор файла fd и команда, уточняющая операцию блокировки. Бло-
Блокировка будет относиться ко всему файлу;
□ произведя системный вызов f cnti (). Параметрами этого вызова являются
дескриптор файла fd, команда, уточняющая операцию блокировки, и ука-
указатель на структуру flock (см. табл. 12.20). Некоторые поля этой структу-
структуры позволяют процессу указать блокируемый участок файла. Таким обра-
образом, процессы могут устанавливать несколько блокировок на разных уча-
участках одного файла.
Как fcntio, так и flock о могут быть использованы с одним и тем же фай-
файлом, причем одновременно. Однако файл, заблокированный с помощью сис-
системного вызова fcntio, не выглядит таковым для fiocko, и наоборот. Это
сделано намеренно, чтобы избежать взаимных блокировок в ситуации, когда
приложение, использующее один тип блокировки, зависит от библиотеки,
использующей другой тип.
Обработка обязательных блокировок файлов происходит чуть сложнее. Не-
Необходимо выполнить следующие действия:
1. Смонтировать файловую систему, в которой требуется обязательная бло-
блокировка, с указанием опции -о mand команды mount. В результате будет ус-
установлен флаг msmandlock в системном вызове mount о. По умолчанию
обязательная блокировка отключена.
2. Пометить файлы как кандидаты на обязательную блокировку, установив
для них бит SGID (установка идентификатора группы) и сбросив бит прав
группы на выполнение файла. Поскольку значение бита установки иден-
идентификатора группы не имеет смысла, когда бит прав группы на выполне-
выполнение файла сброшен, ядро воспринимает такую комбинацию, как намек на
то, что нужно использовать обязательные блокировки вместо рекоменда-
рекомендательных.
3. Сделать системный вызов fcnti О, чтобы установить или снять блокиров-
блокировку файла.
Обработка блокировок типа "аренда" намного проще, чем обработка обяза-
обязательных блокировок. Достаточно сделать системный вызов fcnti () с коман-
командой FSETLEASE ИЛИ FGETLEASE. ПоВТОрНЫЙ ВЫЗОВ fcnti (), ВЫПОЛНеННЫЙ С
командой fsetsig, может быть использован для изменения типа сигнала, по-
посылаемого процессу-держателю блокировки "аренда".
Помимо проверок, выполняемых в системных вызовах read () и write (), ядро
учитывает наличие обязательных блокировок при обслуживании любых сис-
системных вызовов, которые могут изменить содержимое файла. Например, сис-
системный вызов open о с установленным флагом otrunc завершится с ошиб-
ошибкой, если для файла установлена обязательная блокировка.
В следующем разделе описывается основная структура данных, используемая
ядром для обработки блокировок файлов, установленных с помощью систем-
системного вызова flock () (блокировок flflock) и системного вызова fcnti () (бло-
(блокировок FLPOSIX).
Структуры данных для блокировок файлов
Все типы блокировок в Linux представлены одной структурой fileiock,
поля которой показаны в табл. 12.19.
Таблица 12.19. Поля структуры file_lock
Тип Поле Описание
struct f ile_lock * f l_next Следующий элемент в списке
блокировок, связанных
с индексным дескриптором
struct list_head f l_link Указатели для списка активных
или задержанных блокировок
struct list_head f l_block Указатели для списка блокиро-
блокировок, ждущих данную
struct files_struct * fl_owner files_struct владельца
unsigned int f l_pid PID процесса-владельца
wait_quene_head_t f l_wait Очередь ожидания блокирован-
блокированных процессов
struct file * fl_file Указатель на файловый объект
unsigned char f l_f lags Флаги блокировки
unsigned char f l_type Тип блокировки
lof f_t f l_start Начальная точка заблокирован-
заблокированной области
loff_t f l_end Конечная точка заблокирован-
заблокированной области
struct fasync_struc * f l_fasync Используется для оповещений
по поводу блокировки "аренда"
unsigned long f l_break_time Время до окончания блокировки
"аренда"
struct f ile_lock_operations * f l_ops Указатель на операции, выпол-
выполняющие блокировку файлов
struct lock_manager_operations * f l_mops Указатель на операции, управ-
управляющие блокировкой файлов
union f l_u Информация, специфичная для
файловой системы
Все структуры lockf ile, относящиеся к одному и тому же файлу на диске,
собраны в однонаправленный список, на первый элемент которого указывает
поле if lock объекта "индексный дескриптор". Поле finext структуры
lockf ile указывает следующий элемент списка.
Когда некоторый процесс делает блокирующий системный вызов, чтобы по-
получить исключительную блокировку, а на том же самом файле установлены
совместные блокировки, запрос на блокировку не может быть удовлетворен
немедленно, и процесс должен быть временно приостановлен. Тогда он зано-
заносится в очередь, на которую указывает поле fiwait структуры fiieiock,
принадлежащей задержанной блокировке. Два списка служат для проведения
различия между удовлетворенными запросами на блокировку (активными
блокировками) и запросами, которые не могут быть удовлетворены прямо
сейчас (задержанными блокировками).
Активные блокировки собраны в связный "глобальный список блокировок
файлов", первый элемент которого хранится в переменной fileiockiist.
Аналогичным образом все задержанные блокировки собраны в отдельном
связном списке, первый элемент которого хранится в переменной biockiist.
Поле fiiink используется для вставки структуры lockfile в один из двух
списков.
Наконец, и это не менее важно, ядро должно отслеживать все задержанные
блокировки (ждущие), связанные с данной активной блокировкой (задержи-
(задерживающей). Для этой цели предусмотрен список, связывающий все блокировки,
ждущие данную. Поле f ibiock задерживающей блокировки является голов-
головным элементом этого списка, а поля f ibiock ждущих блокировок содержат
указатели на соседние элементы этого списка.
Блокировки FL_FLOCK
Блокировка flflock всегда связана с файловым объектом и, следовательно,
принадлежит процессу, открывшему файл (или всем клонированным процес-
процессам, совместно использующим один открытый файл). Когда блокировка за-
запрошена и предоставлена, ядро меняет любую другую блокировку, имею-
имеющуюся у данного процесса для данного файла, на новую. Это происходит
только в том случае, если процесс хочет сменить блокировку чтения на бло-
блокировку записи, или наоборот. Далее, когда файловый объект освобождается
функцией f put (), все блокировки flflock, ссылающиеся на данный файло-
файловый объект, разрушаются. Однако могут существовать иные блокировки
flflock, установленные другими процессами для того же файла (индексного
дескриптора), и они останутся активными.
Системный вызов flock () позволяет процессу применить или снять рекомен-
рекомендательную блокировку по отношению к открытому файлу. Он имеет два па-
параметра: дескриптор fd файла, на который нужно воздействовать, и and —
параметр, определяющий операцию блокирования. Параметр cmd, имеющий
значение locksh, запрашивает совместную блокировку на чтение, значение
lockex определяет исключительную блокировку на запись, а lockun снимает
блокировку.
( Примечание ^
Фактически системный вызов flock () в состоянии устанавливать обязатель-
обязательные блокировки в совместном режиме с помощью команды lockmand. Однако
мы не будем обсуждать этот случай.
Как правило, этот системный вызов блокирует текущий процесс, если запрос
не может быть удовлетворен немедленно, например, если процессу требуется
исключительная блокировка, в то время как другой процесс ее уже имеет.
Однако если флаг locknb передан вместе с операцией locksh или lockex,
системный вызов не приостанавливает процесс. То есть если блокировка не
может быть получена немедленно, системный вызов возвращает код ошибки.
Когда вызывается служебная процедура sysf lock (), она выполняет следую-
следующие действия:
1. Проверяет, является ли fd допустимым дескриптором файла. Если это не
так, возвращает код ошибки. Получает адрес flip соответствующего фай-
файлового объекта.
2. Проверяет, есть ли у процесса право на чтение/запись в отношении откры-
открытого файла. Если нет, возвращает код ошибки.
3. Получает lock, новый объект fileiock, и инициализирует его должным
образом: поле f ltype устанавливается в соответствии со значением пара-
параметра cmd; в поле fi_file записывается адрес filp файлового объекта; по-
полю f if lags присваивается значение flflock; в поле f i_pid записывается
значение current->tgid; а поле f lend устанавливается в значение -1, что-
чтобы отметить тот факт, что блокирование относится ко всему файлу (а не к
какой-то его части).
4. Если параметр cmd не включает в себя locknb, процедура добавляет в поле
f l_f lags флаг FL_SLEEP.
5. Если для файла определена операция flock, процедура вызывает ее, пере-
передавая в качестве параметров указатель на файловый объект filp, некий
флаг (fsetlkw или fsetlk, в зависимости от значения locknb) и адрес
нового объекта lock типа f ileiock.
6. В противном случае, если файловая система не определяет функцию flock
(что является типичной ситуацией), процедура вызывает fiock_iock_
filewaito, пытаясь выполнить требуемую операцию блокировки. Этой
функции передаются два параметра: filp, указатель на файловый объект, и
адрес нового объекта lock, типа f ileiock, созданного на шаге 3.
7. Если дескриптор fileiock не был занесен в список активных или задер-
задержанных блокировок на предыдущем шаге, процедура освобождает его.
8. В случае успеха возвращает 0.
Функция f lockiockf iiewait () выполняет в цикле следующие действия:
1. Вызывает функцию fiockiockfileo, передавая в качестве параметров
filp, указатель на файловый объект, а также адрес нового объекта lock,
типа fiieiock. Вызванная функция, в свою очередь, выполняет следую-
следующие операции:
• производит поиск в списке, на который указывает fiip->f_dentry->
d_inode->i_flock. ЕСЛИ блокировка FLFLOCK ДЛЯ ТОГО Же фаЙЛОВОГО
объекта найдена, функция проверяет ее тип (locksh или lockex). Если
тип совпадает с типом новой блокировки, функция возвращает 0 (ниче-
(ничего не нужно делать). В противном случае она удаляет старый элемент
из списка блокировок для данного индексного дескриптора и из гло-
глобального списка блокировок файлов, возобновляет выполнение всех
ждущих процессов в очередях блокировок из списка f lbiock и осво-
освобождает Структуру f ilelock;
• если процесс выполняет снятие блокировки (lock un), больше ничего
делать не нужно. Блокировки не было, либо она уже снята. Возвраща-
Возвращается 0;
• если блокировка flflock для того же файлового объекта найдена (то
есть процесс меняет уже имеющуюся блокировку чтения на блокировку
записи или наоборот), то функция предоставляет некоторым высоко-
высокоприоритетным процессам, в частности, каждому процессу, ранее при-
приостановленному по причине старой блокировки, возможность порабо-
поработать, для чего она вызывает функцию condresched ();
• снова выполняет поиск в списке блокировок для данного индексного
дескриптора, чтобы убедиться, что никакая из существующих блокиро-
блокировок flflock не конфликтует с запрошенной. В списке не должно быть
блокировок чтения flflock, а если процесс запрашивает блокировку
чтения, то там вообще не должно быть блокировок flflock;
• если конфликтующие блокировки отсутствуют, функция вставляет но-
новую структуру f ilelock в список блокировок индексного дескриптора
и в глобальный список блокировок файлов, после чего возвращает 0
(успешное завершение);
• если конфликтующая блокировка найдена, происходит следующее. Ес-
Если флаг flsleep в поле f if lags установлен, функция заносит новую
блокировку (ждущую) в циклический список задерживающей блоки-
блокировки и в глобальный список задержанных блокировок;
• возвращает код ошибки -eagain.
2. Проверяет код возврата функции f lockiockf iie ():
• если код возврата 0 (нет конфликтов), возвращает 0 (успешное завер-
завершение);
• обнаружена несовместимость. Если флаг flsleep в поле fifiags
сброшен, функция освобождает дескриптор fileiock и возвращает
-eagain;
• в противном случае, когда несовместимость есть, но процесс может
быть приостановлен, функция ВЫЗЫВает wait_event_interruptible(),
чтобы занести текущий процесс в очередь iock->f lwait и приостано-
приостановить его. Когда выполнение процесса будет возобновлено (сразу после
снятия задерживающей блокировки), функция возвращается к шагу 1 и
повторяет цикл.
Блокировки FL_POSIX
Блокировка flposix всегда связана с процессом и индексным дескриптором;
она автоматически снимается, когда процесс заканчивает выполнение, или
дескриптор файла закрывается (даже если процесс открыл один файл дважды
или создал копию дескриптора файла). Кроме того, блокировки flposix ни-
никогда не наследуются потомком через fork ().
Когда системный вызов f cnti () используется для блокировки файлов, он ра-
работает с тремя параметрами: с дескриптором fd файла, на который следует
воздействовать; с параметром cmd, задающим операцию блокировки; указате-
указателем f 1 на структуру flock, которая хранится в адресном пространстве про-
процесса режима пользователя. Поля этой структуры описаны в табл. 12.20.
( Примечание )
В Linux определена также структура f 1оскб4, в которой поля offset и length
являются 64-битовыми целыми. В последующем изложении мы говорили
о структуре flock, но сказанное справедливо и в отношении f 1оскб4.
Таблица 12.20. Поля структуры flock
Тип Поле Описание
short l_type f_rdlock (запрашивает совместную блокировку), f_wrlock
(запрашивает исключительную блокировку), funlock (снимает
блокировку)
short l_whence seek_set (от начала файла), seek_current (от текущей позиции
в файле), seek_end (от конца файла)
of f_t lstart Начальное смещение заблокированной области относительно
значения l_whence
Таблица 12.20 (окончание)
Тип Поле Описание
of f_t l_len Длина заблокированной области @ означает, что область вклю-
включает все, что будет впоследствии записано в конец файла)
pid_t l_pid PID владельца
Служебная процедура sysfcnti о ведет себя в зависимости от значения фла-
флага, установленного в параметре cmd:
□ fgetlk — определяет, конфликтует ли блокировка, описанная структурой
flock, с какой-либо блокировкой flposix, уже полученной другим про-
процессом. В таком случае в структуру flock записывается информация о су-
существующей блокировке;
□ fsetlk — устанавливает блокировку, описанную структурой flock. Если
блокировка не может быть получена, системный вызов возвращает код
ошибки;
□ fsetlkw — устанавливает блокировку, описанную структурой flock. Если
блокировка не может быть получена, системный вызов блокирует вызвав-
вызвавший процесс, т. е. процесс приостанавливается, пока блокировка не станет
доступной;
□ F_GETLK64, F_SETLK64, F_SETLKW64 анаЛОГИЧНЫ Предыдущим, НО ИСПОЛЬ-
зуют структуру f 1оскб4, а не flock.
Служебная процедура sysfcntio вначале получает файловый объект, соот-
соответствующий параметру fd, а затем вызывает fcntigetiko или
f cntisetik (), в зависимости от команды, переданной в качестве параметра
(fgetlk для первой функции и fsetlk или fsetlkw для второй). Мы рас-
рассмотрим только последний случай.
Функция fcntisetiko принимает три параметра: filp, указатель на файло-
файловый объект, команду cmd (fsetlk или fsetlkw) и указатель на структуру
flock. Функция выполняет следующие действия:
1. Считывает структуру, на которую указывает параметр f 1, в локальную пе-
переменную типа flock.
2. Выясняет, должна ли блокировка быть обязательной, и есть ли у файла со-
совместно используемое отображение в память (см. разд. "Отображение в
память" в главе 16). В этом случае функция отказывается создать блоки-
блокировку и возвращает код ошибки -eagain, поскольку с файлом уже работает
другой процесс.
3. Инициализирует новую структуру f ileiock в соответствии с содержимым
пользовательской структуры flock и с размером файла, указанным в ин-
индексном дескрипторе файла.
4. Если указана команда fsetlkw, функция устанавливает флаг flsleep
в поле f if lags структуры f ileiock.
5. Если поле ltype структуры flock равно frdlck, функция проверяет, есть
ли у процесса право на чтение файла. Аналогичным образом, если ltype
равно fwrlck, проверяется право на запись в файл. Если такого права нет,
возвращается код ошибки.
6. Вызывает метод lock из числа файловых операций, если он определен. Как
правило, в дисковых файловых системах этот метод не определен.
7. Вызывает функцию posix_iock_fiie о, передавая ей в качестве парамет-
параметров адрес объекта "индексный дескриптор" данного файла и адрес объекта
f ileiock. В свою очередь, эта функция выполняет следующие действия:
• вызывает posixiocksconfiictso для каждой блокировки flposix из
списка блокировок данного индексного дескриптора. Вызванная функ-
функция проверяет, не конфликтует ли блокировка с запрошенной. Суть в
том, что в списке индексного дескриптора не должно быть блокировок
flposix на запись для той же области, и не может быть никаких бло-
блокировок flposix для этой области, если процесс запрашивает блоки-
блокировку на запись. Однако блокировки, принадлежащие одному процес-
процессу, не конфликтуют, что позволяет процессу менять характеристики
принадлежащих ему блокировок;
• если конфликтующая блокировка найдена, функция проверяет, был ли
системный вызов f cnti () сделан с флагом fsetlkw. Если это так, теку-
текущий процесс должен быть приостановлен. В этом случае функция вы-
вызывает posix_locks_deadiock(), чтобы проверить, нет ли состояния вза-
взаимной блокировки у процессов, ожидающих блокировки flposix.
Затем она заносит новую блокировку (ждущую) в список, принадле-
принадлежащий конфликтующей (задерживающей) блокировки, и в список за-
задержанных блокировок, после чего возвращает код ошибки. В против-
противном случае, если системный вызов f cnti о сделан с флагом fsetlk,
функция просто возвращает код ошибки;
• если окажется, что список блокировок данного индексного дескриптора
не содержит ни одной конфликтующей блокировки, функция проверяет
все блокировки flposix текущего процесса, затрагивающие область
файла, которую текущий процесс хочет заблокировать, и объединяет
или разбивает на части области файла, вовлеченные в эту ситуацию.
Например, если процесс запросил блокировку на запись для области
файла, расположенной внутри более широкой области, заблокирован-
заблокированной на чтение, то предыдущая блокировка на чтение разделяется на
две, относящиеся к крайним участкам, а на центральный участок на-
накладывается новая блокировка на запись. В случае перекрытия облас-
областей более новые блокировки всегда заменяют старые;
• заносит новую структуру fileiock в глобальный список блокировок
файлов и в список индексного дескриптора;
• возвращает 0 (успешное завершение).
8. Проверяет код возврата функции posix_iock_file ():
• если возвращен 0 (нет конфликтующих блокировок), возвращает О
(успешное завершение);
• обнаружена несовместимость. Если флаг flsleep в поле fifiags
сброшен, функция освобождает дескриптор fileiock и возвращает
-eagain;
• в противном случае, когда несовместимость есть, но процесс может
быть приостановлен, фуНКЦИЯ ВЫЗЫВает wait_event_interruptible (),
чтобы занести текущий процесс в очередь iock->f lwait и приостано-
приостановить его. Когда выполнение процесса будет возобновлено (сразу после
снятия задерживающей блокировки), функция переходит к шагу 7 и
пытается повторить операцию.
ГЛАВА 13
Архитектура ввода/вывода
и драйверы устройств
Виртуальная файловая система, описанная в предыдущей главе, нуждается
в функциях нижнего уровня для выполнения каждой операции чтения, записи
и т. д., в зависимости от конкретного устройства. В главе 12 мы вкратце оста-
остановились на том, как операции выполняются в различных файловых систе-
системах. В этой главе мы рассмотрим, как ядро работает с конкретными устрой-
устройствами.
В разд. "Архитектура ввода/вывода" мы приведем краткое описание архи-
архитектуры ввода/вывода 80x86. Разд. "Модель драйвера устройства" является
введением в модель драйвера устройства, принятую в Linux. Далее, в
разд. "Файлы устройств", мы покажем, как VFS связывает специальный
файл, называемый файлом устройства, с различными аппаратными устройст-
устройствами так, чтобы прикладные программы могли пользоваться любыми типами
устройств унифицированным образом. Затем, в разд. "Драйверы устройств",
мы представим некоторые общие характеристики драйверов. Напоследок, в
разд. "Драйверы символьных устройств", мы проиллюстрируем общую орга-
организацию драйверов символьных устройств в Linux. Обсуждение драйверов
блочных устройств мы отложим до следующей главы.
Читатели, интересующиеся разработкой собственных драйверов устройств,
могут обратиться к книге Джонатана Корбета, Алессандро Рубини и Грега
Кроа-Хартмана "Драйверы устройств в Linux" (Jonathan Corbet, Alessandro
Rubini, and Greg Kroah-Hartman "Linux Device Drivers"), выпущенной изда-
издательством O'Reilly.
Архитектура ввода/вывода
Чтобы компьютер работал как требуется, необходимо наличие путей, по ко-
которым информация курсирует между центральным процессором, оператив-
ной памятью и некоторым количеством устройств ввода/вывода, подключен-
подключенных к компьютеру. Эти пути прохождения данных, называемые шинами, яв-
являются главными коммуникационными каналами внутри компьютера.
У любого компьютера имеется системная шина, которая соединяет большин-
большинство внутренних аппаратных устройств. Типичной системной шиной являет-
является PCI (Peripheral Component Interconnect, Взаимосвязь периферийных ком-
компонентов). На практике применяются и другие виды шин, например, ISA,
EISA, MCA, SCSI и USB. Как правило, в компьютере присутствует несколько
шин различных типов, связанных друг с другом с помощью аппаратных уст-
устройств, называемых мостами. Две высокоскоростные шины предназначены
для передачи данных между элементами памяти: процессорная шина
(Frontside Bus, FSB) соединяет процессор с контроллером ОЗУ, а дополни-
дополнительная шина плана (Backside Bus) соединяет процессор непосредственно с
внешним аппаратным кэшем. Главный мост соединяет системную шину с
процессорной шиной.
Любое устройство ввода/вывода управляется одной и только одной шиной.
Тип шины влияет на внутреннюю конструкцию устройства ввода/вывода и на
то, как к устройству обращается ядро. В этом разделе мы обсудим функцио-
функциональные характеристики, общие для всех архитектур ПК, не углубляясь в де-
детали, присущие конкретным типам шин.
Путь прохождения данных от ЦП до устройства ввода/вывода обычно назы-
называется шиной ввода/вывода. В микропроцессорах 80x86 шестнадцать адрес-
адресных контактов используются для адресации устройств ввода/вывода, а 8, 16
или 32 контакта данных — для передачи данных. Со своей стороны, шина
Рис. 13.1. Архитектура ввода/вывода в ПК
ввода/вывода соединена с каждым устройством ввода/вывода с помощью
иерархически организованной структуры, состоящей из трех элементов: пор-
портов ввода/вывода, интерфейсов и контроллера. Архитектура ввода/вывода
показана на рис. 13.1.
Порты ввода/вывода
Каждое устройство, соединенное с шиной ввода/вывода, имеет собственный
набор адресов, называемых портами ввода/вывода. В архитектуре IBM PC
адресное пространство ввода/вывода включает в себя до 65 536 8-битовых
портов ввода/вывода. Два соседних 8-битовых порта можно считать одним
16-битовым портом, который должен начинаться с четного адреса. Анало-
Аналогично, два соседних 16-битовых порта можно считать одним 32-битовым
портом, который должен начинаться с адреса, кратного четырем. Специаль-
Специальные команды ассемблера, in, ins, out и outs, позволяют центральному про-
процессору считывать данные с порта ввода/вывода и записывать их в порт. Вы-
Выполняя одну из этих команд, процессор выбирает нужный порт и пересылает
данные между своим регистром и портом.
Порты ввода/вывода могут быть отображены в адреса физического адресного
пространства. После этого процессор может общаться с устройством вво-
ввода/вывода при помощи команд, работающих непосредственно с памятью (та-
(таких как mov, and, or и т. д.). Современные устройства лучше подходят для от-
браженного ввода/вывода, поскольку он выполняется быстрее и может соче-
сочетаться с DM А-доступом.
Одной из важных целей, стоящих перед разработчиками систем, является
создание унифицированного подхода к программированию ввода/вывода без
понижения производительности. Стремясь к этому, они разделяют порты
ввода/вывода на несколько специализированных регистров, как показано на
рис. 13.2. Процессор пишет команды, которые следует отправить устройству,
в управляющий регистр устройства. Значение, показывающее внутреннее
Рис. 13.2. Специализированные порты ввода/вывода
состояние устройства, процессор считывает с регистра состояния устройства.
Кроме того, процессор получает данные от устройства, считывая байты с
входного регистра, а передает данные устройству, записывая байты в выход-
выходной регистр.
Для уменьшения затрат один и тот же порт ввода/вывода часто используется
для разных целей. Например, некоторые биты описывают состояние устрой-
устройства, а другие — команду, выдаваемую устройству. Кроме того, порт вво-
ввода/вывода может служить и входным, и выходным регистром.
Обращение к портам ввода/вывода
Команды ассемблера in, out, ins и outs дают доступ к портам ввода/вывода.
Для упрощения такого доступа в ядро включены следующие служебные
функции:
□ inb (), inw (), ini () — прочитать, соответственно, один, два или четыре по-
последовательных байта из порта ввода/вывода. Суффиксы "b", Mw" и "
обозначают байт (8 битов), слово A6 битов) и длинное слово C2 бита) со-
соответственно;
□ inbj? (), inwp (), inip () — прочитать, соответственно, один, два или че-
четыре последовательных байта из порта ввода/вывода, а затем выполнить
"пустую" команду, чтобы создать паузу;
□ outb (), outw (), outi () — записать, соответственно, один, два или четыре
последовательных байта в порт ввода/вывода;
□ outbpo, outwpo, outlpo — записать, соответственно, один, два или
четыре последовательных байта в порт ввода/вывода, а затем выполнить
"пустую" команду, чтобы создать паузу;
□ insb(), insw(), insio — прочитать из порта ввода/вывода последователь-
последовательность групп, состоящих соответственно из одного, двух или четырех бай-
байтов. Длина последовательности передается функции в качестве параметра;
□ outsbo, outsw(), outsio — записать в порт ввода/вывода последователь-
последовательность групп, состоящих соответственно из одного, двух или четырех бай-
байтов.
В то время как доступ к портам ввода/вывода довольно прост, определить,
какие порты ввода/вывода назначены устройствам, не так легко, особенно для
систем с шиной ISA. Нередко драйверу устройства приходится вслепую пи-
писать данные в какой-нибудь порт ввода/вывода, чтобы прозондировать аппа-
аппаратное устройство. Однако если этот порт уже используется другим устрой-
устройством, может произойти крах системы. Для предотвращения подобных си-
ситуаций ядро отслеживает порты ввода/вывода для каждого аппаратного
устройства, пользуясь механизмом "ресурсов".
Ресурс — это часть некой сущности, которая может быть эксклюзивно назна-
назначена драйверу устройства. В нашем случае ресурс представляет собой диапа-
диапазон адресов портов ввода/вывода. Информация, относящаяся к каждому ре-
ресурсу, хранится в структуре resource, поля которой представлены в
табл. 13.1. Все ресурсы одного типа заносятся в древовидную структуру. На-
Например, ресурсы, представляющие диапазоны адресов портов ввода/вывода,
включены в дерево, корнем которого является ioportresourse.
Таблица 13.1. Поля структуры resourse
Тип Поле Описание
const char * name Описание владельца ресурса
unsigned long start Начало диапазона ресурса
unsigned long end Конец диапазона ресурса
unsigned long flags Различные флаги
struct resourse * parent Указатель на родителя в дереве ресурсов
struct resourse * sibling Указатель на узел того же уровня в дереве ресурсов
struct resourse * child Указатель на первого потомка в дереве ресурсов
Потомки узла собраны в список, на первый элемент которого указывает поле
child. Поле sibling указывает на следующий узел в списке.
Почему используется дерево? Рассмотрим, например, адреса порта вво-
ввода/вывода в интерфейсе жесткого диска IDE. Предположим, они лежат в диа-
диапазоне от Oxfooo до OxfOOf. Ресурс, у которого поле start имеет значение
Oxf ооо, а поле end — значение Oxf oof, включается в древовидную структуру, а
условное имя контроллера записывается в поле name. Однако драйвер IDE
должен помнить дополнительную информацию, а именно тот факт, что под-
поддиапазон от Oxfooo до Oxf007 используется для master-диска в цепочке IDE, a
поддиапазон от 0xf008 до OxfOOf— для slave-диска. С этой целью драйвер
вставляет два узла-потомка ниже ресурса, соответствующего всему диапазо-
диапазону от Oxf ооо до OxfOOf (по одному потомку для каждого поддиапазона портов
ввода/вывода). В качестве общего правила можно сказать, что каждый узел
дерева должен соответствовать поддиапазону диапазона, ассоциированного
с родительским узлом. Корень дерева ресурсов портов ввода/вывода
(ioportresourse) охватывает все адресное пространство (от 0 до 65 535).
Каждый драйвер устройства может вызывать следующие три функции, пере-
передавая им корневой узел дерева ресурсов и адрес интересующей его структуры
данных:
□ requestresourse () — присваивает данный диапазон устройству ввода/вы-
ввода/вывода;
□ aiiocateresourseo — ищет в дереве ресурсов доступный диапазон,
имеющий указанный размер и тип выравнивания. Если такой диапазон
существует, функция присваивает его устройству ввода/вывода. Преиму-
Преимущественно используется драйверами PCI-устройств, которые могут быть
настроены на произвольные номера портов и отображаемые адреса па-
памяти;
П reieaseresourseo — освобождает указанный диапазон, ранее присвоен-
присвоенный устройству ввода/вывода.
Кроме того, в ядре определены псевдонимы для этих функций, относящиеся
к портам ввода/вывода: requestregion () присваивает указанный диапазон
портов, a reieaseregion () освобождает ранее присвоенный диапазон портов
ввода/вывода. Дерево адресов ввода/вывода, присвоенных устройствам на
данный момент, хранится в файле /proc/ioports.
Интерфейсы ввода/вывода
Интерфейс ввода/вывода— это электронная схема, расположенная между
группой портов ввода/вывода и соответствующим контроллером устройства.
Интерфейс преобразует значения на портах ввода/вывода в команды и дан-
данные для устройства. Действуя в обратном направлении, он определяет изме-
изменения в состоянии устройства и обновляет содержимое порта ввода/вывода,
который в этом случае играет роль регистра состояния. Эта электронная схе-
схема может быть также соединена через линию IRQ с программируемым кон-
контроллером прерываний для выдачи запросов на прерывания от имени уст-
устройства.
Существует два типа интерфейсов:
□ специализированные интерфейсы ввода/вывода — предназначены для од-
одного конкретного аппаратного устройства. В некоторых случаях контрол-
контроллер устройства расположен на той же плате (картеI, что и интерфейс.
Устройства, подсоединенные к специализированному интерфейсу, могут
быть либо внутренними (установленными в корпусе компьютера), либо
внешними (находящимися вне корпуса);
□ интерфейсы ввода/вывода общего назначения— используются для под-
подключения различных аппаратных устройств. Как правило, к интерфейсам
общего назначения подключаются внешние устройства.
1 Каждая плата должна быть вставлена в один из свободных слотов шины компьютера. Если плата
может быть соединена с внешним устройством с помощью кабеля, то у нее имеется соответствую-
соответствующий разъем на задней панели компьютера.
Заказные интерфейсы ввода/вывода
Чтобы читатель получил представление о том, насколько разнообразны спе-
специализированные интерфейсы ввода/вывода (а следовательно, и устройства,
установленные в компьютерах), мы перечислим самые распространенные:
□ Интерфейс клавиатуры — соединен с контроллером клавиатуры, который
содержит специализированный микропроцессор. Этот микропроцессор
декодирует комбинацию нажатых клавиш, генерирует прерывание и зано-
заносит соответствующий скан-код во входной регистр.
□ Графический интерфейс— установлен вместе с соответствующим
контроллером на графической карте, имеющей собственный фрейм-буфер,
а также специализированный процессор и некоторый программный код,
записанный на чип постоянного запоминающего устройства. Фрейм-
буфер — это память карты, хранящая описание текущего содержимого эк-
экрана.
□ Дисковый интерфейс — соединен кабелем с контроллером диска, который
обычно интегрирован с самим диском. Например, интерфейс IDE соеди-
соединен при помощи 40-жильного плоского кабеля с интеллектуальным кон-
контроллером, встроенным в накопитель.
□ Шинный интерфейс мыши — соединен кабелем с соответствующим кон-
контроллером, встроенным в мышь.
□ Сетевой интерфейс — установлен вместе с соответствующим контролле-
контроллером на сетевой карте, которая служит для приема или передачи сетевых
пакетов. Хотя существует несколько признанных сетевых стандартов,
Etherner (IEEE 802.3) является самым распространенным.
Интерфейсы ввода/вывода общего назначения
В современных компьютерах имеется несколько интерфейсов общего назна-
назначения, которые соединяются с широким кругом внешних устройств. Приве-
Приведем самые распространенные интерфейсы:
□ Параллельный порт — традиционно служит для соединения с принтерами,
но может быть использован и для подключения съемных дисководов, ска-
сканеров, устройств резервного копирования и других компьютеров. Данные
пересылаются порциями в 1 байт (8 битов).
□ Последовательный порт — аналогичен параллельному, но данные пересы-
пересылаются по одному биту. Включает в себя чип UART (Universal Asyn-
Asynchronous Receiver and Transmitter, Универсальное асинхронное приемо-
приемопередающее устройство) для разбиения передаваемых байтов на биты и
сборки принятых битов в байты. Будучи по природе своей медленнее па-
параллельного порта, этот интерфейс используется, главным образом, для
соединения с внешними устройствами, не работающими на больших ско-
скоростях, такими как модемы, мыши и принтеры.
□ Интерфейс PCMCIA — применяется, в основном, в портативных компью-
компьютерах. Внешнее устройство, по виду напоминающее кредитную карточку,
может быть вставлено в слот и вынуто без перезагрузки системы. Самыми
распространенными устройствами PCMCIA являются дисководы, модемы,
сетевые карты и расширения памяти.
□ Интерфейс SCSI (Small Computer System Interface, Интерфейс малых ком-
компьютерных систем)— это электронное устройство, которое соединяет
главную шину компьютера со вспомогательной, называемой SCSI-шиной.
Шина SCSI-2 позволяет объединить до восьми компьютеров и внешних
устройств, таких как жесткие диски, сканеры, пишущие приводы и т. д.
Широкие интерфейсы SCSI-2 и SCSI-3 позволяют подсоединить 16 и бо-
более устройств, если имеются дополнительные интерфейсы. Стандарт
SCSI— это коммуникационный протокол, применяемый для соединения
устройств с помощью SCSI-шины.
О USB (Universal Serial Bus, Универсальная последовательная шина) — это
высокоскоростной интерфейс ввода/вывода общего назначения, который
может служить для соединения с устройствами, традиционно подключае-
подключаемыми к параллельному порту, последовательному порту и интерфейсу
SCSI.
Контроллеры устройств
Для управления сложным устройством может потребоваться контроллер уст-
устройства. По сути, контроллер используется в двух целях:
□ он интерпретирует команды высокого уровня, принятые от интерфейса
ввода/вывода, и заставляет устройство выполнять специфические дейст-
действия, посылая ему последовательности электрических сигналов;
□ он преобразует и соответствующим образом интерпретирует электриче-
электрические сигналы, принятые от устройства, и изменяет (посредством интер-
интерфейса ввода/вывода) значение регистра состояния.
Типичным контроллером устройства является контроллер диска, который
принимает команды высокого уровня (например, "записать этот блок дан-
данных") от микропроцессора при посредстве интерфейса ввода/вывода и преоб-
преобразует их в дисковые операции низкого уровня (например, "расположить
магнитную головку над соответствующей дорожкой" и "записать данные на
эту дорожку"). Современные контроллеры дисков очень сложны, поскольку
они умеют хранить данные диска во встроенных дисковых кэшах и переупо-
рядочивать высокоуровневые запросы от ЦП, чтобы оптимизировать их под
геометрию конкретного диска.
У простых устройств нет контроллеров. Примерами являются программи-
программируемый контроллер прерываний (см. главу 4) и программируемый таймер ин-
интервалов (см. главу 6).
Некоторые аппаратные устройства имеют собственную память, которая часто
называется совместно используемой памятью ввода/вывода. Например, во
всех современных графических картах есть фрейм-буфер с памятью объемом
в десятки мегабайт, в котором хранятся экранные изображения, подлежащие
выводу на монитор. Мы обсудим совместно используемую память вво-
ввода/вывода далее в этой главе.
Модель драйвера устройства
Ранние версии ядра Linux предлагали разработчикам драйверов не так уж
много базовых функциональных возможностей: выделение динамической
памяти, резервирование диапазона адресов ввода/вывода или линии IRQ, ак-
активизация служебной процедуры обработки прерываний в ответ на прерыва-
прерывание от устройства. Правда и то, что старые аппаратные устройства были гро-
громоздкими, программировать их было неудобно, а два разных устройства име-
имели мало общего, даже если располагались на одной шине. Таким образом, не
было особого смысла предлагать разработчикам драйверов какую-либо уни-
унифицированную модель.
Сейчас все обстоит иначе. Типы шин, такие как PCI, предъявляют строгие
требования к конструкции аппаратных устройств. Как следствие, современ-
современные устройства, даже принадлежащие к разным классам, обладают сходными
функциональными возможностями. Драйверы таких устройств, как правило,
должны уделять внимание:
□ управлению питанием (обработке разных уровней напряжения на линии
питания устройства);
□ технологии plug-and-play (прозрачному распределению ресурсов при кон-
конфигурировании устройства);
□ "горячему" подключению (поддержке подсоединения и отсоединения уст-
устройства во время работы системы).
Управление питанием выполняется ядром глобально для каждого устройства
в системе. Например, когда компьютер, питающийся от аккумулятора, пере-
переходит в режим ожидания, ядро должно перевести каждое аппаратное устрой-
устройство (жесткие диски, графическую и звуковую карты, сетевую карту, кон-
контроллеры шин и т. д.) в режим пониженного потребления энергии. Таким об-
разом, драйвер каждого устройства, которое может быть переведено в режим
ожидания, должен включать в себя функцию обратного вызова, которая пе-
переводит устройство в режим пониженного потребления энергии. Более того,
устройства должны быть переведены в режим ожидания в строго определен-
определенном порядке, иначе некоторые из них останутся в ненадлежащем состоянии.
Например, ядро должно вначале перевести в состояние ожидания сначала
диски, а затем их контроллер, потому что в противном случае будет невоз-
невозможно отправить дискам нужные команды.
Чтобы можно было реализовать подобные операции, Linux 2.6 предоставляет
разработчикам несколько структур данных и вспомогательных функций,
обеспечивающих унифицированное представление всех шин, устройств и
драйверов устройств в системе. Этот "каркас" называется моделью драйвера
устройства.
Файловая система sysfs
Файловая система sysfs является специальной файловой системой, аналогич-
аналогичной /ргос, которая обычно монтируется на каталоге /sys. Система /ргос была
первой специальной файловой системой, созданной для того, чтобы разре-
разрешать приложениям, работающим в режиме пользователя, обращаться к внут-
внутренним структурам данных ядра. У файловой системы /sysfs, в сущности, те
же цели, но она предоставляет дополнительную информацию о структурах
данных ядра. Кроме того, система /sysfs имеет более структурированную ор-
организацию, чем /ргос. Вероятно, обе будут мирно сосуществовать в ближай-
ближайшем будущем.
Цель файловой системы sysfs состоит в показе иерархических отношений
среди элементов модели драйвера устройства. На верхнем уровне эта файло-
файловая система имеет следующие каталоги:
□ block — блочные устройства, независимо от того, к какой шине они под-
подключены;
□ devices — все аппаратные устройства, распознанные ядром; они сгруппи-
сгруппированы в зависимости от шины, к которой подключены;
П bus — шины, к которым подключены устройства;
□ drivers — драйверы устройств, зарегистрированные в ядре;
□ class — типы устройств в системе (звуковые карты, сетевые карты, графи-
графические карты и т. д.); к одному классу могут быть отнесены устройства,
подключенные к разным шинам и управляемые разными драйверами;
□ power— файлы, используемые для обработки режимов энергопотребле-
энергопотребления некоторых аппаратных устройств;
□ firmware — файлы, используемые для управления встроенным программ-
программным обеспечением некоторых аппаратных устройств.
Отношения между компонентами моделей драйверов устройств представле-
представлены в файловой системе sysfs в виде символьных ссылок между каталогами и
файлами. Например, файл /sys/block/sda/device может быть символьной ссыл-
ссылкой на подкаталог, вложенный в каталог /sys/devices/pci0000:00, который
представляет SCSI-контроллер, подключенный к шине PCI. Кроме того, файл
/sys/block/sda/device/block является символьной ссылкой на /sys/block/sda, от-
отмечая тот факт, что данное PCI-устройство является контроллером SCSI-
диска.
Основная роль обычных файлов в файловой системе sysfs заключается в
представлении атрибутов драйверов и устройств. Например, файл dev в ката-
каталоге /sys/block/hda содержит старший и младший номера master-диска в пер-
первой цепочке IDE.
Объекты kobject
Основной структурой данных в модели драйвера устройства является общая
структура, называемая kobject и неразрывно связанная с файловой системой
sysfs: каждый объект kobject соответствует каталогу в этой файловой систе-
системе. Объекты kobject вложены в более крупные объекты — так называемые
контейнеры, которые описывают компоненты модели драйвера устройства2.
Дескрипторы шин, устройств и драйверов являются типичными контейнера-
контейнерами. Например, дескриптор первого раздела на первом IDE-диске соответству-
соответствует каталогу /sys/block/hda/hdal.
Встраивание объекта kobject в контейнер позволяет ядру:
□ поддерживать счетчик ссылок на контейнер;
□ поддерживать иерархические списки или наборы контейнеров (например,
каталог системы sysfs, ассоциированный с блочным устройством, включа-
включает в себя различные подкаталоги для каждого раздела на диске);
□ обеспечивать пользовательское представление для атрибутов контейнера.
Объекты kobject, наборы kset и подсистемы
Объект kobject представлен структурой kobject, поля которой перечислены
в табл. 13.2.
2 Объекты kobject используются, главным образом, для реализации модели драйвера устройства.
Однако в настоящее время предпринимаются усилия по изменению некоторых других элементов
ядра, — таких как подсистема модулей, — чтобы можно было пользоваться ими и там.
Таблица 13.2. Поля структуры kobject
Тип Поле Описание
char * k_name Указатель на строку с именем контейнера
char [] name Строка с именем контейнера, если длина имени не
превышает 20 байтов
struct k_ref kref Счетчик ссылок на контейнер
struct list_head entry Указатель для списка, в который заносятся объекты
kobject
struct kobject * parent Указатель на родительский kobject, если таковой
имеется
struct kset * kset Указатель на включающий объект набор kset
struct kobj_type * ktype Указатель на дескриптор типа kobject
struct dentry * dentry Указатель на элемент каталога файла sysfs,
ассоциированного с объектом kobject
Поле ktype указывает на объект kobjtype, представляющий "тип" объекта
kobject, точнее, тип контейнера, содержащего kobject. Структура kobjtype
состоит из трех полей: метода release (выполняемого, когда kobject освобож-
освобождается), указателя sysfsops на таблицу операций файловой системы sysfs и
списка атрибутов по умолчанию этой файловой системы.
Поле kref является структурой типа kref, состоящей из единственного поля
refcount. Как можно догадаться по его имени, это счетчик ссылок на объект
kobject, но оно может быть также использовано как счетчик ссылок на кон-
контейнер объекта. Функции kobjectgeto и kobjectputо, соответственно,
увеличивают и уменьшают счетчик ссылок. Когда значение счетчика дости-
достигает нуля, ресурсы, используемые объектом kobject, освобождаются, и вы-
выполняется метод release объекта kobjtype данного объекта kobject. Этот
метод, обычно определяемый только, когда контейнер kobject выделяется ди-
динамически, освобождает сам контейнер.
Объекты kobject могут быть организованы в дерево с помощью наборов kset.
kset — это собрание объектов kobject одного типа, т. е. входящих в контейнер
этого типа. Поля структуры kset перечислены в табл. 13.3.
Таблица 13.3. Поля структуры kset
Тип Поле Описание
struct subsystem subsys Указатель на дескриптор подсистемы
struct kobj_type * ktype Указатель на дескриптор типа объекта
kobject из данного набора kset
Таблица 13.3 (окончание)
Тип Поле Описание
struct list_head list Голова списка объектов kobject, входящих
в набор kset
struct kobject kobj Встроенный kobject
struct kset_hotplug_ops * hotplug_ops Указатель на таблицу функций обратного
вызова для фильтрования и "горячего"
подключения
Поле list является головой двунаправленного циклического списка объек-
объектов kobject, входящих в kset. Поле ktype указывает на дескриптор kobj type,
используемый совместно всеми объектами kobject в kset.
Поле kobj является объектом kobject, встроенным в структуру kset. Поля
parent объектов kobject, входящих в kset, указывают на этот встроенный
kobject. Таким образом, kset является собранием объектов kobject, но он ис-
использует объект kobject более высокого уровня для подсчета ссылок и уста-
установления связей в дереве. Такой подход позволяет создавать высокоэффек-
высокоэффективный код и обеспечивает значительную гибкость. Например, функции
ksetget () и ksetput (), которые, соответственно, увеличивают и уменьшают
счетчик ссылок набора kset, просто вызывают функции kobjectgeto и
kobjectput о встроенного объекта kobject, поскольку счетчик ссылок kset
является не более чем счетчиком ссылок объекта kobj, встроенного в этот
kset. Более того, благодаря встроенному объекту, структура kset может быть
встроена в объект-контейнер точно так же, как это делается для структуры
kobject. Наконец, kset можно сделать элементом другого набора kset: доста-
достаточно вставить встроенный объект kobject в набор kset более высокого уровня.
Существуют также и собрания наборов kset, называемые подсистемами. Под-
Подсистема может включать в себя наборы kset разных типов, и она представлена
структурой subsystem, состоящей всего из двух полей:
□ kset — встроенный kset, содержащий наборы kset, входящие в подсисте-
подсистему;
□ rwsem— семафор чтения/записи, защищающий все наборы и объекты, ре-
рекурсивно включенные в подсистему.
Даже структура subsystem может быть встроена в более крупный объект-
контейнер. Счетчик ссылок такого контейнера является счетчиком ссылок
встроенной подсистемы, т. е. счетчиком ссылок объекта kobject, встроенного
в набор kset, встроенный в подсистему. Функции subsysget () и subsysput (),
соответственно, увеличивают и уменьшают этот счетчик ссылок.
На рис. 13.3 изображен пример иерархической структуры, являющейся мо-
моделью драйвера устройства. Подсистема bus включает в себя подсистему pci,
которая, в свою очередь, содержит набор drivers. Этот набор содержит объект
serial (соответствующий драйверу последовательного порта), у которого име-
имеется ОДИН атрибут newid.
Рис. 13.3. Пример иерархической структуры, являющейся моделью драйвера устройства
Регистрация объектов kobject, наборов kset и подсистем
В качестве общего правила, можно сказать, что, если вам нужно, чтобы объ-
объект kobject, набор kset или подсистема появились в поддереве sysfs, вы долж-
должны их сначала зарегистрировать. Каталог, ассоциированный с объектом
kobject, всегда появляется в каталоге родительского объекта. Например, ката-
каталоги объектов kobject, включенных в один и тот же набор kset, появляются в
каталоге самого kset. Следовательно, структура поддерева sysfs представляет
иерархию отношений между различными зарегистрированными объектами
kobject, а значит, и между различными контейнерами. Обычно каталоги верх-
верхнего уровня файловой системы sysfs ассоциированы с зарегистрированными
подсистемами.
Функция kobjectregister о инициализирует объект kobject и добавляет со-
соответствующий каталог в файловую систему sysfs. Перед вызовом этой
функции вызывающий код должен установить поле kset в структуре kobject
так, чтобы оно указывало на родительский kset, если таковой имеется. Функ-
Функция kobjectunregistero удаляет каталог объекта kobject из файловой
системы sysfs. Чтобы облегчить жизнь программистам ядра, Linux предла-
предлагает еще И функции kset_register() И kset_unregister (), а также
subsystem_register () И subsystem_unregister (), НО ОНИ ЯВЛЯЮТСЯ всего ЛИШЬ
интерфейсными ДЛЯ kobject_register () И kobject_unregister ().
Как было сказано ранее, многие каталоги объектов kobject содержат обычные
файлы, называемые атрибутами. Функция sysfscreatefileo принимает в
качестве параметра адреса объекта kobject и дескриптора атрибута и создает
специальный файл в соответствующем каталоге. Другие отношения между
объектами, представленными в файловой системе sysfs, устанавливаются с
помощью символьных ссылок: функция sysfscreateiinko создает сим-
символьную ссылку для данного объекта kobject в каталоге, ассоциированном
с другим объектом kobject.
Компоненты модели драйвера устройства
Модель драйвера устройства образована из небольшого количества структур,
представляющих шины, устройства, драйверы устройств и т. д. Рассмот-
Рассмотрим их.
Устройства
Каждое устройство в модели драйвера устройства представлено объектом
device, поля которого приведены в табл. 13.4.
Таблица 13.4. Поля объекта device
Тип Поле Описание
struct listhead node Указатели для списка устройств
того же уровня
struct listjnead bus_list Указатели для списка устройств,
подключенных к шинам одного
типа
struct listhead driverlist Указатели для списка устройств
данного драйвера
struct listhead children Голова списка дочерних устройств
struct device * parent Указатель на родительское
устройство
struct kobject * kobj Встроенный kobject
char [ ] busid Положение устройства на шине
Таблица 13.4 (окончание)
Тип Поле Описание
struct bus_type * bus Указатель на шину
struct device_driver * driver Указатель на драйвер устройства
void * driver_data Указатель на закрытые данные
для драйвера
void * platform_data Указатель на закрытые данные
для унаследованных драйверов
устройств
struct dev_pm_info power Информация, связанная с
управлением питанием
unsigned long detach_state Режим энергопотребления,
в который надо перейти при
выгрузке драйвера
unsigned long long * dma_mask Указатель на DMA-маску
устройства
unsigned long long coherent_dma_mask Маска когерентного прямого
доступа к памяти для данного
устройства
struct list_head dma_pools Голова списка составных
DMA-буферов
struct dma_coherent_mem * dma_mem Указатель на дескриптор
когерентной DMA-памяти,
используемый устройством
void (*) (struct device *) release Функция обратного вызова для
освобождения дескриптора
устройства
Объекты device собираются глобально в подсистеме devicessubsys, ассо-
ассоциированной с каталогом /sys/devices. Устройства организованы иерархиче-
иерархически: устройство является "родителем" некоторых "дочерних" устройств, если
те не могут без него работать корректно. Например, в компьютере, построен-
построенном на PCI, мост между PCI-шиной и USB-шиной является родительским
устройством для каждого устройства, подключенного к шине USB. Поле
parent объекта device указывает на дескриптор родителя, поле children явля-
является головой списка дочерних устройств, а поле node содержит указатели на
соседние элементы в списке дочерних устройств. Отношения "предок-
потомок" между объектами kobject, встроенными в объекты device, тоже от-
отражают иерархию устройств. Таким образом, структура каталогов, располо-
расположенных ниже /sys/devices, отражает физическую организацию аппаратных
устройств.
Каждый драйвер поддерживает список объектов device, соответствующих
устройствам, которыми он управляет. Поле driveriist объекта device со-
содержит указатели на соседние элементы, а поле driver указывает на дескрип-
дескриптор драйвера устройства. Для каждого типа шины существует список, вклю-
включающий в себя все устройства, подключенные к шинам данного типа. Поле
busiist объекта device содержит указатели на соседние элементы, а поле bus
указывает на дескриптор типа шины.
Счетчик ссылок позволяет отслеживать обращения к объекту device; он вхо-
входит в состав объекта kobj, встроенного в дескриптор. Счетчик увеличивается
При ВЫЗОВе фуНКЦИИ get_device () И уменьшается При ВЫЗОВе put_device ().
Функция deviceregister () вставляет новый объект device в модель драйвера
устройства и автоматически создает для него новый каталог в каталоге
/sys/devices. Функция deviceunregister о, наоборот, удаляет устройство из
модели драйвера.
Как правило, объект device статически встраивается в более крупный деск-
дескриптор. Например, устройства PCI описываются структурами pcidev. Поле
dev этой структуры является объектом device, а другие поля специфичны для
ШИНЫ PCI. Функции deviceregister () И deviceunregister () ВЫПОЛНЯЮТСЯ
при регистрации и дерегистрации устройства в PCI-слое ядра.
Драйверы
Каждый драйвер в модели драйверов устройств описывается объектом
devicedriver, поля которого приведены в табл. 13.5.
Таблица 13.5. Поля объекта device_driver
Тип Поле Описание
char * name Имя драйвера устройства
struct bus_type * bus Указатель на дескриптор шины, к которой
подключаются поддерживаемые устрой-
устройства
struct semaphore unload_sem Семафор, запрещающий выгрузку драй-
драйвера; он освобождается, когда счетчик
ссылок достигает нуля
struct kobject * kobj Встроенный kobject
struct list_head devices Голова списка, включающего в себя все
устройства, поддерживаемые драйвером
struct module * owner Идентифицирует модуль, который реали-
реализует драйвер устройства, если таковой
имеется
Таблица 13.5 (окончание)
Тип Поле Описание
int (*) (struct device *) probe Метод для зондирования устройства
(проверки того, что драйвер может управ-
управлять им)
int (*) (struct device *) remove Метод, вызываемый при удалении уст-
устройства
void (*) (struct device *) shutdown Метод, вызываемый при полном отклю-
отключении питания устройства
int (*) (struct device *, suspend Метод, вызываемый, когда устройство
unsigned long, unsigned переводится в режим ожидания
long)
int (*)(struct device *, resume Метод, вызываемый, когда устройство
unsigned long) возвращается в нормальное состояние
(режим полного энергопотребления)
Объект devicedriver имеет четыре метода для обработки "горячего" под-
подключения, поддержки plug-and-play и управления питанием. Метод probe вы-
вызывается, когда драйвер шины обнаруживает устройство, которым, вероятно,
может управлять данный драйвер. Соответствующая функция должна про-
прозондировать аппаратную часть, чтобы выполнить дальнейшую проверку уст-
устройства. Метод remove вызывается для устройства, допускающего "горячее"
подключение, когда оно отсоединяется. Он также вызывается для каждого
устройства, управляемого драйвером, когда выгружается сам драйвер. Мето-
Методы shutdown, suspend И resume вызываются ДЛЯ устройства, КОГДа ядро ДОЛЖНО
изменить его режим энергопотребления.
Счетчик ссылок, включенный в объект kobj, встроенный в дескриптор, по-
позволяет отслеживать обращения к объекту devicedriver. Счетчик увеличива-
увеличивается при вызове функции getdrivero и уменьшается при вызове
put_driver().
ФуНКЦИЯ driver_register() вставляет НОВЫЙ объект devicedriver В МОДеЛЬ
драйвера устройства и автоматически создает для него новый каталог в фай-
файловой системе sysfs. И наоборот, функция driver unregister о удаляет драй-
драйвер из модели.
Как правило, объект devicedriver статически встраивается в более крупный
дескриптор. Например, драйверы PCI описываются структурами pcidriver.
Поле driver этой структуры является объектом devicedriver, а другие поля
специфичны для шины PCI.
Шины
Каждый тип шины, поддерживаемый ядром, описывается объектом bustype.
Его поля перечислены в табл. 13.6.
Таблица 13.6. Поля объекта bus_type
Тип Поле Описание
char * name Название типа шины
struct subsystem subsys Подсистема объектов kobject, связанная
с этим типом шины
struct kset drivers Набор объектов драйверов
struct kset devices Набор объектов устройств
struct bus_attribute * busattrs Указатель на объект, содержащий атрибу-
атрибуты шины и методы для экспорта их в фай-
файловую систему sysfs
struct device_attribute * dev_attrs Указатель на объект, содержащий атрибу-
атрибуты устройства и методы для экспорта их
в файловую систему sysfs
struct driverattribute * drvattrs Указатель на объект, содержащий
атрибуты драйвера и методы для экспорта
их в файловую систему sysfs
int (*) (struct device *, match Метод для проверки, поддерживает ли
struct device_driver *) данный драйвер данное устройство
int (*) (struct device *, hotplug Метод, вызываемый, когда устройство
char **, int, char *, int) регистрируется
int (*) (struct device *, suspend Метод для сохранения состояния аппарат-
unsigned long) ного контекста и изменения режима энер-
энергопотребления устройства
int (*) (struct device *) resume Метод для изменения режима энергопо-
энергопотребления и восстановления аппаратного
контекста устройства
Каждый объект bustype включает в себя встроенную подсистему. Подсисте-
Подсистема, которая хранится в переменной bussubsys, объединяет все подсистемы,
встроенные в объекты bustype. Подсистема bussubsys ассоциирована с ка-
каталогом /sys/bus. Например, существует каталог /sys/bus/pci, ассоциирован-
ассоциированный с типом шины PCI. Подсистема, относящаяся к конкретной шине, как
правило, содержит только два набора kset, называемых drivers и devices
(и Соответствующих ПОЛЯМ drivers И devices объекта bustype).
Набор drivers содержит дескрипторы devicedriver всех драйверов, имею-
имеющих отношение к данному типу шины, а набор devices содержит дескрипто-
ры device всех устройств шины данного типа. Поскольку каталоги, соответ-
соответствующие объектам устройств, уже содержатся в файловой системе sysfs в
каталоге /sys/devices, каталог устройств подсистемы, относящейся к данной
шине, содержит символьные ссылки на каталоги, включенные в /sys/devices.
Функции busf oreachdrv () И busf oreachdev () перебирают Элементы СПИ-
СПИСКОВ драйверов и устройств соответственно.
Метод match выполняется, когда ядро должно проверить, может ли данный
драйвер управлять данным устройством. Функция, реализующая этот метод,
как правило, проста, потому что она ищет идентификатор устройства в таб-
таблице идентификаторов, поддерживаемых драйвером. Метод hotplug выпол-
выполняется, когда устройство регистрируется в модели драйвера. Функция, реали-
реализующая этот метод, должна использовать переменные окружения для переда-
передачи информации, специфичной для шины, программе, работающей в режиме
пользователя, которая таким образом оповещается о наличии нового доступ-
доступного устройства (см. разд. "Регистрация драйвера устройства" далее в этой
главе). Наконец, методы suspend и resume выполняются, когда устройство на
шине данного типа должно поменять режим энергопотребления.
Классы
Каждый класс описывается объектом class. Все объекты классов принадле-
принадлежат подсистеме ciasssubsys, ассоциированный с каталогом /sys/class. Кроме
того, каждый объект class содержит встроенную подсистему. Например, су-
существует каталог /sys/class/input, ассоциированный с классом input в модели
драйвера устройства.
Каждый Объект class ВКЛЮЧает В себя СПИСОК дескрипторов class_device,
каждый из которых представляет одно логическое устройство, принадлежа-
принадлежащее к этому классу. Структура classdevice имеет поле dev, которое указыва-
указывает на дескриптор device. Таким образом, логическое устройство всегда ссы-
ссылается на некоторое устройство в модели драйвера устройства. Однако воз-
возможно НаЛИЧИе НеСКОЛЬКИХ ДеСКрИПТОрОВ classdevice, ОТНОСЯЩИХСЯ К
одному устройству. В реальности аппаратное устройство может состоять из
нескольких различных подустройств, каждому из которых требуется свой
интерфейс режима пользователя. Например, звуковая карта является аппа-
аппаратным устройством, которое обычно содержит DSP, микшер, интерфейс
игрового порта и т. д. Каждое такое подустройство требует отдельный ин-
интерфейс режима пользователя и поэтому ассоциировано с собственным
каталогом файловой системы sysfs.
Предполагается, что драйверы устройств одного класса предоставляют сход-
сходные функциональные возможности приложениям режима пользователя. На-
Например, все драйверы звуковых карт должны предлагать способ записи зву-
звуковых сэмплов на DSP.
В модели драйвера устройства классы, в первую очередь, нацелены на пре-
предоставление стандартного метода для экспорта интерфейсов логических
устройств в приложения режима пользователя. В каждый дескриптор
class device встроен объект kobject, имеющий атрибут (специальный файл)
по имени dev. Этот атрибут содержит старший и младший номера файла уст-
устройства, необходимого для обращения к соответствующему логическому
устройству.
Файлы устройств
Как было сказано в главе 7, в основе Unix-подобных операционных систем
лежит понятие файла, который является всего лишь контейнером для инфор-
информации, структурированным в виде последовательности байтов. В соответст-
соответствии с таким подходом, устройства ввода/вывода считаются специальными
файлами, называемыми файлами устройств. Таким образом, системные вы-
вызовы, выполняемые при взаимодействии с обычными файлами на диске, мо-
могут применяться и для непосредственного взаимодействия с устройствами
ввода/вывода. Например, один и тот же системный вызов write () может быть
использован для записи данных в обычный файл и для отправки их на прин-
принтер. Во втором случае необходимо данные записать в файл устройства
/dev/lpO.
Принимая во внимание характеристики соответствующих драйверов, можно
разделить файлы устройств на два типа: блочные и символьные. Различия
между двумя классами аппаратных устройств не такие четкие. Во всяком
случае, можно предполагать следующее:
□ в блочном устройстве возможен произвольный доступ к данным, а время,
необходимое для передачи блока данных, мало и всегда примерно одина-
одинаково, по крайней мере, с точки зрения человека. Типичными примерами
блочных устройств являются жесткие диски, гибкие диски, CD-ROM и
DVD;
□ к данным на символьном устройстве либо вовсе нельзя обратиться произ-
произвольным образом (пример — звуковая карта), либо произвольный доступ
возможен, но время обращения сильно зависит от позиции элемента дан-
данных внутри устройства (пример — накопитель на магнитной ленте).
Сетевые карты являются замечательным исключением из этой схемы, потому
что они являются аппаратными устройствами, не связанными напрямую
с файлами устройств.
Файлы устройств использовались уже в первых версиях операционной сис-
системы Unix. Обычно файл устройства— это реальный файл, включенный
в файловую систему. Однако его индексный дескриптор необязательно со-
держит указатели на блоки данных на диске (то есть данные, хранящиеся в
файле), потому что их попросту нет. Вместо этого индексный дескриптор
должен содержать идентификатор аппаратного устройства, соответствующе-
соответствующего файлу символьного или блочного устройства.
Традиционно этот идентификатор состоит из названия типа файла устройства
(то есть символьный или блочный) и пары чисел. Первое число, называемое
старшим номером, идентифицирует тип устройства. По традиции все файлы
устройств, имеющих одинаковый старший номер и одинаковый тип, имеют и
одинаковый набор файловых операций, поскольку управляются одним драй-
драйвером. Второе число, называемое младшим номером, идентифицирует кон-
конкретное устройство в группе устройств, имеющих одинаковый старший но-
номер. Например, если несколько дисков управляется одним контроллером, то
у них один старший номер, но разные младшие номера.
Системный вызов mknodo служит для создания файлов устройств. Он прини-
принимает в качестве параметров имя файла устройства, его тип и старший и
младший номера. Файлы устройств обычно находятся в каталоге /dev.
В табл. 13.7 приводятся атрибуты некоторых файлов устройств. Обратите
внимание, что символьные и блочные устройства имеют независимую нуме-
нумерацию, и блочное устройство C.0) отличается от символьного устройст-
устройства C.0).
Таблица 13.7. Примеры файлов устройств
I"- |сГС|";°""р"й|°—
/dev/fdO Блочное 2 0 Накопитель на гибких дисках
/dev/hda Блочное 3 0 Первый диск IDE
/dev/hda2 Блочное 3 2 Второй первичный раздел первого
диска IDE
/dev/hdb Блочное 3 64 Второй диск IDE
/dev/hdb3 Блочное 3 67 Третий первичный раздел второго
диска IDE
/dev/ttypO Символьное 3 0 Терминал
/dev/console Символьное 5 1 Консоль
/dev/lp1 Символьное 6 1 Параллельный принтер
/dev/ttySO Символьное 4 64 Первый последовательный порт
/dev/rtc Символьное 10 135 Часы реального времени
/dev/null Символьное 1 3 Фиктивное устройство
Как правило, файл устройства ассоциирован с аппаратным устройством (на-
(например, /dev/hda — с жестким диском) или с какой-то физической или логи-
логической составляющей аппаратного устройства (например, /dev/hda2 — с раз-
разделом на диске). Однако в некоторых случаях файл устройства не ассоцииро-
ассоциирован ни с каким реальным устройством, а представляет фиктивное логическое
устройство. Например, /dev/null — это файл, соответствующий "черной ды-
дыре"; все данные, записываемые в него, просто отбрасываются, и файл всегда
пуст.
Что касается ядра, ему безразлично имя файла. Если вы создадите блочный
файл устройства с именем /tmp/disk, старшим номером, равным 3, и младшим
номером, равным 0, то он будет эквивалентен файлу /dev/hda из таблицы.
С другой стороны, имена файлов могут быть важны для некоторых приклад-
прикладных программ. Например, какая-нибудь коммуникационная программа может
предполагать, что первый последовательный порт ассоциирован с файлом
устройства /dev/ttySO. Впрочем, большинство прикладных программ можно
сконфигурировать так, что они будут работать с произвольными именами
файлов устройств.
Работа с файлами устройств
в режиме пользователя
В традиционных системах Unix (и в ранних версиях Linux) старший и млад-
младший номера файла устройства имели длину 8 битов. Таким образом, допуска-
допускалось существование максимум 65 536 блочных файлов устройств и столько
же символьных. Казалось бы, такого количества достаточно, но, к сожале-
сожалению, это не так.
Реальная проблема заключается в том, что файлы устройств традицион-
традиционно размещаются раз и навсегда в каталоге /dev. Следовательно, каж-
каждое логическое устройство в системе должно иметь соответствующий файл
с хорошо продуманной нумерацией. Официальный реестр выделенных
номеров устройств и подкаталогов каталога /dev хранится в файле
Documentation/devices.txt; макросы, соответствующие старшим номерам
устройств можно также найти в файле include/linux/major.h.
К сожалению, количество различных аппаратных устройств в наши дни так
велико, что почти все номера устройств уже выделены. Официальный реестр
номеров устройств хорошо работает в средних системах Linux, однако может
не подходить для широкомасштабных систем. Более того, в современных
системах могут быть установлены сотни или тысячи дисков одного типа, и
8-битового младшего номера уже будет недостаточно. Например, в реестре
зарезервированы номера устройств для 16 SCSI-дисков, каждый из которых
имеет 15 разделов. Если в системе установлено больше 16 SCSI-дисков, то
стандартное назначение старших и младших номеров должно быть изменено.
Это нетривиальная задача, которая требует модификации исходного кода яд-
ядра и затрудняет сопровождение системы.
Чтобы решить эту проблему, размер номеров устройств в Linux 2.6 был уве-
увеличен. Теперь старший номер занимает 12 битов, а младший— 20. Оба но-
номера обычно хранятся в 32-битовой переменной типа devt. Макросы major и
minor извлекают соответственно старший и младший номер из этой перемен-
переменной, а макрос mkdev записывает в нее два номера. В целях обратной совмес-
совместимости ядро корректно обрабатывает старые файлы устройств, имеющие 16-
битовые номера устройств.
Дополнительные номера устройств не распределены статически в официаль-
официальном реестре, потому что они нужны лишь при необычных требованиях,
предъявляемых к номерам устройств. Современные способы работы с файла-
файлами устройств отличаются динамичностью в отношении присваивания номе-
номеров устройствам и создания файлов устройств.
Динамическое присваивание номеров устройствам
Каждый драйвер устройства на этапе регистрации указывает диапазон номе-
номеров, с которыми он собирается работать (см. разд. "Регистрация драйвера
устройства" далее в этой главе). Впрочем, драйвер может потребовать вы-
выделения интервала номеров без указания конкретных значений. В этом случае
ядро выделяет подходящий диапазон номеров и назначает их драйверу.
Следовательно, драйверам современных устройств больше не требуется ин-
информация из официального реестра номеров, поскольку они могут просто
воспользоваться номерами, доступными в системе на данный момент.
Однако в таком случае файл устройства не может быть создан раз и навсегда.
Он должен создаваться сразу после инициализации драйвера с надлежащими
старшим и младшим номерами. Поэтому необходим стандартный способ экс-
экспортирования номеров устройств, используемых каждым драйвером, в при-
приложения, работающие в режиме пользователя. Как мы видели ранее, модель
драйвера устройства предлагает элегантное решение: старший и младший
номера хранятся в атрибутах dev в подкаталогах каталога /sys/class.
Динамическое создание файлов устройств
Ядро Linux может создавать файлы устройств динамически. Нет необходимо-
необходимости заполнять каталог /dev файлами всех мыслимых аппаратных устройств,
поскольку файлы устройств могут быть созданы "по требованию". Благодаря
модели драйвера устройства ядро Linux 2.6 позволяет сделать это весьма про-
простым способом. В системе необходимо установить набор программ режима
пользователя, известный под коллективным именем "инструментарий udev".
При запуске системы содержимое каталога /dev стирается, а программа udev
сканирует подкаталоги каталога /sys/class в поисках файлов dev. Для каждого
такого файла (который представляет собой комбинацию из старшего и млад-
младшего номеров логического устройства, поддерживаемого ядром) программа
создает соответствующий файл устройства в каталоге /dev. Кроме того, она
назначает имена файлам устройств и создает символьные ссылки согласно
информации в файле конфигурации, чтобы была соблюдена традиционная
схема именования файлов устройств в Unix. В конце концов, каталог /dev за-
заполняется файлами устройств, поддерживаемых ядром данной системы, и
только ими.
Нередко файл устройства создается уже после инициализации системы. Это
происходит либо при загрузке модуля, содержащего драйвер ранее не под-
поддерживаемого устройства, либо при "горячем" подключении устройства, на-
например, к порту USB. Инструментарий udev может автоматически создать
соответствующий файл устройства, потому что модель драйвера устройства
поддерживает "горячее" подключение. Как только ядро обнаруживает новое
устройство, оно запускает новый процесс, выполняющий сценарий оболочки
/sbin/hotplug, работающий в режиме пользователя3, передавая ему полезную
информацию об обнаруженном устройстве через переменные окружения.
Сценарий режима пользователя обычно читает файл конфигурации и выпол-
выполняет операции, необходимые для завершения инициализации нового устрой-
устройства. Если инструментарий udev установлен, этот сценарий создает также со-
соответствующий файл устройства в каталоге /dev.
Работа с файлами устройств в VFS
Файлы устройств находятся в дереве каталогов системы, но по сути своей
отличаются от обычных файлов и каталогов. Когда процесс обращается к
обычному файлу, он фактически обращается к блокам данных на разделе
диска, используя файловую систему. Когда же он обращается к файлу уст-
устройства, он фактически управляет аппаратным устройством. Например, про-
процесс может обратиться к файлу устройства, чтобы прочитать показания элек-
электронного термометра, подключенного к компьютеру, и узнать температуру
в помещении. VFS отвечает за сокрытие разницы между файлами устройств
и обычными файлами от прикладных программ.
С этой целью VFS заменяет файловые операции по умолчанию у файла уст-
устройства, когда он открывается. Как результат, каждый системный вызов для
3 Путь к программе режима пользователя, вызываемой при "горячем" подключении, может быть
изменен в файле /proc/sys/kernel/hotplug. (Более того, в современных дистрибутивах его функцио-
функциональность также переложена на инструментарий udev. — Прим. науч. ред.)
файла устройства транслируется в вызов функции, специфичной для устрой-
устройства, а не функции файловой системы. Функция, специфичная для устройст-
устройства, воздействует на устройство, чтобы оно выполнило операцию, запрошен-
запрошенную процессом4.
Предположим, что процесс делает системный вызов open () для файла устрой-
устройства (блочного или символьного). Операции, выполняемые этим системным
вызовом, описаны в разд. "Системный вызов ореп()" главы 12. Фактически,
соответствующая служебная процедура анализирует путь к файлу устройства
и устанавливает необходимые объекты: индексный дескриптор, элемент ка-
каталога и файловый объект.
Индексный дескриптор инициализируется путем чтения соответствующего
индексного дескриптора с диска при помощи подходящей функции файловой
системы (обычно ЭТО ext2_read_inode () ИЛИ ext3_read_inode (); СМ. главу 18).
Когда эта функция обнаруживает, что индексный дескриптор на диске отно-
относится к файлу устройства, она вызывает функцию initspeciaiinode (), ко-
которая инициализирует поле irdev индексного дескриптора значениями
старшего и младшего номеров файла устройства. Кроме того, она записывает
в поле iop индексного дескриптора адрес одной из таблиц файловых опера-
операций: либо defbikfops, либо defchrfops, в зависимости от типа файла уст-
устройства. Служебная процедура системного вызова open о вызывает также
функцию dentryopeno, которая выделяет новый файловый объект и записы-
записывает в его поле fop адрес, хранящийся в if op, т. е. адрес либо defbikfops,
либо defchrfops. Благодаря этим двум таблицам, каждый системный вызов,
сделанный для файла устройства, активизирует функцию драйвера устройст-
устройства, а не функцию файловой системы.
Драйверы устройств
Драйвер устройства — это набор процедур ядра, который заставляет аппа-
аппаратное устройство реагировать на программный интерфейс, определяемый
канОНИЧеСКИМ набором функций VFS (open, read, lseek, ioctl И Т. Д.), КОТОрые
используются для управления устройством. Фактическая реализация возлага-
возлагается на драйвер. Поскольку разные устройства имеют разные контроллеры
ввода/вывода и, следовательно, разные наборы команд и разную информацию
о состоянии, у большинства устройств ввода/вывода есть собственные драй-
драйверы.
Существует много типов драйверов устройств. В основном, они различаются
уровнем поддержки, предлагаемой приложениям в режиме пользователя, а
4 Обратите внимание, что, благодаря механизму анализа пути, описанному в разд. "Анализ пути"
главы 12, символьные ссылки на файлы устройств работают точно так же, как и файлы устройств.
также стратегиями буферизации данных, собираемых с аппаратных уст-
устройств. Поскольку эти особенности драйверов сильно зависят от их внутрен-
внутренней структуры, мы обсудим их в разд. "Прямой доступ к памяти (DMA) " и
"Стратегии буферизации для символьных устройств".
Драйвер устройства состоит не только из функций, реализующих операции
файла устройства. Перед тем как драйвером можно будет пользоваться, необ-
необходимо произвести определенные действия. Мы рассмотрим их в следующих
разделах.
Регистрация драйвера устройства
Нам известно, что каждый системный вызов для файла устройства трансли-
транслируется ядром в вызов подходящей функции соответствующего драйвера.
Чтобы это произошло, драйвер устройства должен зарегистрировать себя.
Другими словами, регистрация драйвера означает выделение нового дескрип-
дескриптора devicedriver, занесение его в структуры модели драйвера устройства и
связывание его с соответствующим файлом (или файлами) устройства. Об-
Обращения к файлам устройств, чьи драйверы не были предварительно зареги-
зарегистрированы, завершаются с кодом ошибки -enodev.
Если драйвер устройства статически вкомпилирован в ядро, его регистрация
выполняется на этапе инициализации ядра. Если же драйвер откомпилирован
как модуль ядра (см. приложение 2), его регистрация выполняется при за-
загрузке модуля. В последнем случае драйвер может отменить собственную
регистрацию при выгрузке модуля.
Рассмотрим в качестве примера типичное PCI-устройство. Чтобы правильно
управлять им, его драйвер должен выделить дескриптор типа pcidriver, ко-
который используется PCI-слоем ядра для управления устройством. После ини-
инициализации некоторых полей этого дескриптора драйвер вызывает функцию
pciregisterdriver о. Фактически, дескриптор pcidriver включает в себя
встроенный дескриптор devicedriver, а функция pciregisterf unction ()
просто инициализирует поля встроенного дескриптора драйвера и вызывает
функцию driverregistero, чтобы занести драйвер в структуры модели
драйвера устройства.
Когда драйвер устройства проходит регистрацию, ядро ищет не поддержи-
поддерживаемые аппаратные устройства, которыми, вероятно, мог бы управлять этот
драйвер. Для этого оно использует метод match () соответствующего дескрип-
дескриптора ТИПа ШИНЫ bustype И метод probe объекта devicedriver. ЕСЛИ устрой-
ство, которым может управлять драйвер, обнаружено, ядро выделяет объект
device И вызывает функцию deviceregister (), чтобы занести ЭТО устройство
в модель драйвера устройства.
Инициализация драйвера устройства
Регистрация драйвера устройства и его инициализация — абсолютно разные
действия. Драйвер регистрируется как можно раньше, чтобы приложения,
выполняющиеся в режиме пользователя, могли работать с ним через файлы
устройств. В противоположность этому, инициализируется драйвер как мож-
можно позднее. Инициализация драйвера фактически означает выделение ему
ценных ресурсов системы, которые вследствие этого станут недоступны дру-
другим драйверам.
Мы уже видели подобный пример в главе 4: назначение линий IRQ устройст-
устройствам обычно происходит динамически, непосредственно перед их использова-
использованием, потому что одна линия IRQ может потребоваться нескольким устрой-
устройствам. Другим примером ресурсов, выделяемых в самый последний момент,
являются страничные кадры для буферов DMA и сам канал DMA (для старых
устройств, не работающих с PCI, таких как привод гибких дисков).
Для гарантии того, что ресурсы будут получены, когда необходимо, но не
затребованы лишний раз, когда они уже выделены, драйверы устройств
обычно действуют по следующей схеме:
□ счетчик обращений отслеживает количество процессов, обратившихся к
файлу устройства. Счетчик увеличивается методом open файла устройства
и уменьшается методом release5;
□ метод open проверяет значение счетчика перед увеличением. Если счетчик
равен нулю, драйвер устройства должен выделить ресурсы и включить
прерывания и DMA на аппаратном устройстве;
□ метод release проверяет значение счетчика после уменьшения. Если счет-
счетчик равен нулю, никакие процессы больше не пользуются данным аппа-
аппаратным устройством. В таком случае метод отключает прерывания и DMA
на контроллере ввода/вывода, а затем освобождает выделенные ресурсы.
Мониторинг операций ввода/вывода
Продолжительность операции ввода/вывода часто непредсказуема. Она мо-
может зависеть от механических факторов (текущей позиции головки по отно-
отношению к блоку, который должен быть передан), от действительно случайных
событий (прихода пакета данных на сетевую карту) или от человеческого
фактора (от того, когда пользователь нажмет на клавишу или когда он заме-
5 Точнее, счетчик обращений отслеживает количество файловых объектов, ссылающихся на файл
устройства, поскольку клонированные процессы могут совместно обращаться к одному файловому
объекту.
тит, что бумага застряла в принтере). В любом случае драйвер устройства,
который начал операцию ввода/вывода, должен использовать мониторинг,
чтобы понять, что операция ввода/вывода завершена, или закончилось время
ожидания.
Если операция завершилась, драйвер читает значение регистра состояния ин-
интерфейса ввода/вывода, чтобы определить, удачно ли прошла операция вво-
ввода/вывода. В том случае, когда истекло время ожидания, драйвер "понимает",
что случилась неприятность, поскольку максимальное время, необходимое
для выполнения операции, закончилось, но ничего не произошло.
Для мониторинга завершения операции ввода/вывода применяются две мето-
методики. Они называются режимом опроса и режимом прерывания.
Режим опроса
В соответствии с этой методикой, процессор периодически проверяет (опра-
(опрашивает) регистр состояния устройства, пока значение регистра не просигна-
просигнализирует об окончании операции ввода/вывода. Мы уже сталкивались с под-
подходом, основанным на опросе, в главе 5: когда процессор пытается получить
занятый спин-блокировку, он периодически опрашивает переменную, пока ее
значение не станет нулевым. Однако опрос, выполняемый в отношении опе-
операций ввода/вывода, обычно очень трудоемкий, потому что драйверу нужно
еще не забывать проверять, не истекло ли время ожидания. Простой пример
опроса выглядит так:
for (;;) {
if (read_status(device) & DEVICE_END_OPERATION) break;
if (—count == 0) break;
}
Переменная count, проинициализированная до входа в цикл, уменьшается на
каждом шаге и может быть использована для реализации грубого механизма
проверки времени ожидания. Более точный механизм можно реализовать,
читая значения счетчика тиков jiffies на каждом шаге цикла (см. главу 6)
и сравнивая его со старым значением, прочитанным до входа в цикл ожи-
ожидания.
Если время, необходимое для завершения операции ввода/вывода, относи-
относительно велико, скажем, порядка нескольких миллисекунд, эта схема стано-
становится не эффективной, потому что процессор непроизводительно тратит дра-
драгоценные машинные циклы, ожидая окончания операции. В таких случаях
предпочтительно волевым решением освобождать процессор после каждой
операции опроса, вставив внутрь цикла вызов функции schedule ().
Режим прерывания
Режим прерывания можно использовать, только если контроллер ввода/вы-
ввода/вывода способен просигнализировать по линии IRQ об окончании операции.
Мы продемонстрируем работу режима прерывания в простейшем случае.
Предположим, мы хотим реализовать драйвер для простого символьного уст-
устройства ввода. Когда пользователь делает системный вызов read () для соот-
соответствующего файла устройства, команда на ввод отправляется управляюще-
управляющему регистру устройства. По истечении некоторого времени устройство по-
помещает один байт данных в свой входной регистр. Затем драйвер устройства
возвращает этот байт в качестве результата системного вызова read ().
Это типичный случай, в котором предпочтительно реализовать драйвер с ис-
использованием режима прерываний. В сущности, такой драйвер состоит из
двух функций:
□ f ooread () — реализует метод read файлового объекта;
□ f oointerrupt () — обрабатывает прерывание.
Функция f ooread () вызывается, когда пользователь читает файл устройства:
ssize_t foo_read(struct file *filp, char *buf, size_t count,
loff_t *ppos)
{
foo_dev_t foo_dev = filp->private_data;
if (down_interruptible(&foo_dev->sem)
return -ERESTARTSYS;
foo_dev->intr = 0;
outb(DEV_FOO_READ, DEV_FOO_CONTROL_PORT);
wait_event interruptible(foo dev->wait, (foo dev->intr ==1));
if (put_user(foo_dev->data, buf))
return -EFAULT;
UP(&foo_dev->sem);
return 1;
}
Драйвер устройства использует специализированный дескриптор типа
foodevt, который включает в себя семафор sem, защищающий аппаратное
устройство от попыток одновременного обращения, очередь ожидания wait,
флаг intr, устанавливаемый, когда устройство выдает прерывание, и одно-
однобайтовый буфер data, в который пишет обработчик прерываний и из которого
читает метод read. Вообще, все драйверы ввода/вывода, применяющие пре-
прерывания, пользуются структурами данных, к которым обращаются как обра-
обработчик прерываний, так И методы read И write. Адрес дескриптора f oo_dev_t
обычно хранится в поле privatedata файлового объекта, принадлежащего
файлу устройства, или в глобальной переменной.
Функция f ooread () выполняет следующие операции:
□ Получает семафор foo_dev->sem, чтобы никакой другой процесс не обра-
обратился к устройству.
□ Сбрасывает флаг intr.
□ Выдает устройству ввода/вывода команду на чтение.
□ Выполняет макрос wait_event_interruptible, чтобы приостановить про-
цесс, пока флаг intr не станет равным 1. Этот макрос описан в главе 3.
По прошествии некоторого времени наше устройство выдает прерывание,
сигнализируя, что операция ввода/вывода завершена, и данные находятся в
соответствующем порте devfoodataport. Обработчик прерываний устанав-
устанавливает флаг intr, чтобы возобновить выполнение процесса. Когда планиров-
планировщик решает продолжить выполнение процесса, выполняется вторая часть
ФУНКЦИИ f oo_read () I
1. Копирует символ, находящийся в переменной foo_dev->data, в адресное
пространство пользователя.
2. Освобождает семафор f oo_dev->sem и завершается.
Ради простоты мы не проверяем, истекло ли время ожидания. Вообще говоря,
проверка времени ожидания реализуется с помощью статических или дина-
динамических таймеров (см. главу 6). Таймер должен быть установлен в соответ-
соответствующее значение непосредственно перед началом операции ввода/вывода и
удален, когда операция завершится.
Рассмотрим КОД функции foo_interrupt() :
irqreturn_t foo_interrupt(int irq, void *dev_id, struct pt regs *)
{
foo->data = inb(DEVLFOO_DATA_PORT);
foo->intr = 1;
wake_up_interruptible(foo_dev->wait);
return 1;
}
Обработчик прерываний считывает символ с входного регистра нашего уст-
устройства и записывает его в поле data дескриптора f oodevt драйвера устрой-
устройства, на которое указывает глобальная переменная f оо. Затем он устанавлива-
устанавливает флаг intr И Вызывает функцию wake_up_interruptible(), Чтобы ВОЗОбнО-
вить выполнение процесса, заблокированного в очереди foo->wait.
Обратите внимание, что ни один из трех параметров в обработчике прерыва-
прерывания не используется. Это довольно распространенная ситуация.
Обращение к совместно используемой памяти
ввода/вывода
В зависимости от устройства и типа шины, совместно используемая память
ввода/вывода в архитектуре компьютера может быть отображена в различные
диапазоны физических адресов. Как правило, имеют место следующие си-
ситуации.
Для большинства устройств, подключенных к шине ISA, совместно исполь-
используемая память ввода/вывода отображается в 16-битовые физические адреса в
диапазоне от Охаоооо до Oxffff. Таким образом, образуется "дыра" от
640 Кбайт до 1 Мбайт, о которой говорилось в главе 2.
Для устройств, подключенных к шине PCI, совместно используемая память
ввода/вывода отображается в 32-битовые физические адреса вблизи границы
4 Гбайт. Управлять такими устройствами намного проще.
Несколько лет назад компания Intel представила стандарт AGP (Accelerated
Graphics Port, Ускоренный графический порт), который является развитием
PCI для высокопроизводительных графических карт. В дополнение к собст-
собственной совместно используемой памяти ввода/вывода, такая карта способна
напрямую обращаться к участкам оперативной памяти на материнской плате
при помощи специальной электронной схемы GART (Graphics Address Re-
Remapping Table, Таблица преобразования графических адресов). Схема GART
позволяет AGP-картам достигать намного большей скорости передачи дан-
данных, чем у старых PCI-карт. Впрочем, ядру, на самом деле, безразлично, где
расположена физическая память, и память, отображенная согласно GART,
для него не отличается от других типов совместно используемой памяти вво-
ввода/вывода.
Как драйвер устройства обращается к тому или иному участку совместно ис-
используемой памяти ввода/вывода? Начнем с архитектуры PC, с которой разо-
разобраться проще, а затем включим в обсуждение и другие архитектуры.
Вспомните, что программы ядра пользуются линейными адресами, и адреса в
совместно используемой памяти ввода/вывода можно выразить числами, пре-
превышающими pageoffset. Далее мы будем предполагать, что значение
pageoffset равно Охсооооооо, т. е. линейные адреса ядра расположены в чет-
четвертом гигабайте.
Драйверы устройств должны перевести физические адреса совместно исполь-
используемой памяти ввода/вывода в линейные адреса в пространстве ядра. В архи-
архитектуре PC этого можно достичь, выполнив логическую операцию ИЛИ над
32-битовым физическим адресом и константой Охсооооооо. Предположим,
например, что ядру нужно сохранить в переменной ti значение, расположен-
расположенное в памяти ввода/вывода по физическому адресу 0xc000b0fe4, а в перемен-
ной t2 — значение, расположенное по адресу Oxfcoooooo. Казалось бы, сле-
следующие два оператора справятся с этой задачей:
tl = *((unsigned char *)@xc00b0fe4));
t2 = *((unsigned char *)(OxfcOOOOOO));
На этапе инициализации ядро отображает доступные физические адреса опе-
оперативной памяти в начальный участок четвертого гигабайта линейного ад-
адресного пространства. Следовательно, блок управления страницами отобра-
отобразит линейный адрес 0xc00b0fe4, указанный в первом операторе, обратно в
оригинальный физический адрес памяти ввода/вывода 0xc000b0fe4, который
попадает в "дыру ISA" между 640 Кбайт до 1 Мбайт (см. главу 2). Все работа-
работает как нельзя лучше.
А вот со вторым оператором возникают проблемы, потому что физический
адрес ввода/вывода больше максимального физического адреса оперативной
памяти системы. Следовательно, линейный адрес Oxf соооооо не соответствует
физическому адресу Oxfcoooooo. В таких случаях Таблицы Страниц ядра
должны быть модифицированы так, чтобы они включали в себя линейный
адрес, который отображается на физический адрес ввода/вывода. Это можно
сделать, вызвав функцию ioremapo или ioremapnocache (). Первая, во мно-
многом аналогичная функции vmaiioco, вызывает getvmarea (), чтобы создать
новый дескриптор vmstruct (см. главу 8) для интервала линейных адресов,
имеющего размер, равный размеру требуемой области совместно используе-
используемой памяти ввода/вывода. Затем функции обновляют соответствующие запи-
записи в канонических Таблицах Страниц ядра. Функция ioremapnocache () отли-
отличается от ioremap () тем, что она, кроме прочего, отключает аппаратный кэш,
когда ссылается на переотображенные линейные адреса.
Итак, корректный вид второго оператора должен быть примерно таким:
io_mem = ioremap(OxfЬ000000, 0x200000);
t2 = *((unsigned char *)(io_mem + 0x100000));
Здесь первый оператор создает двухмегабайтовый интервал линейных адре-
адресов, отображающий физические адреса, начиная с Oxfboooooo, а второй читает
содержимое памяти по адресу Oxfcoooooo. Чтобы впоследствии снять отобра-
отображение, драйвер устройства должен вызвать функцию iounmap ().
В некоторых других архитектурах, отличных от архитектуры PC, к совместно
используемой памяти ввода/вывода нельзя обратиться путем простого разы-
разыменования линейного адреса, указывающего на место в физической памяти.
Поэтому в Linux определены следующие архитектурно-независимые функ-
функции, которые следует вызывать при обращении к совместно используемой
памяти ввода/вывода:
□ readb (), readw (), readi () — читают, соответственно один, два или четыре
байта из совместно используемой памяти ввода/вывода;
□ writeb (), writew (), writei () — записывают, соответственно, один, два или
четыре байта в совместно используемую память ввода/вывода;
□ memcpy_f romio (), memcpy_toio () — КОПИруЮТ блок данных ИЗ СОВМеСТНО
используемой памяти ввода/вывода в динамическую память и, соответст-
соответственно, наоборот.
□ memsetioO — заполняет область совместно используемой памяти вво-
ввода/вывода фиксированным значением.
Рекомендуется следующий способ обращения к участку памяти ввода/вывода
с адресом Oxfcoooooo:
io_mem = ioremap(OxfbOOOOOO, 0x200000);
t2 = readb(io_mem + 0x100000));
Благодаря этим функциям удается скрыть специфику платформенно-
зависимых способов обращения к совместно используемой памяти вво-
ввода/вывода.
Прямой доступ к памяти (DMA)
В оригинальной архитектуре PC центральный процессор является единствен-
единственным "хозяином" шины в системе. То есть это единственное аппаратное уст-
устройство, управляющее адресной шиной данных при чтении и сохранении
значений в ячейках оперативной памяти. В современных архитектурах шин,
таких как PCI, каждое периферийное устройство может быть хозяином шины,
если имеет соответствующую электронную схему. В настоящее время все
персональные компьютеры снабжены схемами DMA, которые обеспечивают
обмен данными между оперативной памятью и устройством ввода/вывода.
После того как центральный процессор активизирует схему DMA, она спо-
способна продолжать пересылку данных самостоятельно. Как пересылка закон-
закончится, DMA сгенерирует запрос на прерывание. Конфликты, возникающие,
когда процессору и схеме DMA одновременно нужна одна и та же область
памяти, разрешаются электронной схемой, называемой арбитром памяти
(см. главу 5).
Прямой доступ к памяти используется, в основном, драйверами дисков и дру-
других устройств, передающих большое количество байтов за одну операцию.
Поскольку время включения схемы DMA относительно велико, то при пере-
передаче небольшого количества байтов эффективнее использовать центральный
процессор.
Первые схемы DMA для старых ISA-шин были сложны, трудны для про-
программирования и ограничены нижними 16 Мбайт физической памяти. Со-
Современные схемы DMA для шин PCI и SCSI взаимодействуют со специали-
зированными схемами в самих шинах, что облегчает жизнь разработчикам
драйверов.
Синхронный и асинхронный режимы DMA
Драйвер устройства может использовать схему DMA двумя различными спо-
способами, которые называются синхронным DMA и асинхронным DMA. В пер-
первом случае передачу данных инициируют процессы, во втором — аппаратные
устройства.
Примером синхронного DMA является воспроизведение аудиотрека звуковой
картой. Приложение, работающее в режиме пользователя, записывает аудио-
аудиоданные, называемые сэмплами, в файл устройства, ассоциированный с про-
процессором цифрового сигнала на звуковой карте. Драйвер звуковой карты на-
накапливает эти сэмплы в буфере ядра. Одновременно драйвер выдает звуковой
карте инструкцию копировать сэмплы из буфера ядра в процессор цифрового
сигнала с четко заданной синхронизацией. Когда звуковая карта заканчивает
пересылку данных, она возбуждает прерывание, и драйвер проверяет, есть ли
еще в буфере ядра сэмплы, которые нужно воспроизвести. Если есть, то он
активизирует еще одну пересылку данных с использованием прямого доступа
к памяти.
Пример асинхронного прямого доступа к памяти — ситуация, в которой се-
сетевая карта принимает из сети пакет данных. Периферийное устройство со-
сохраняет пакет в своей совместно используемой памяти ввода/вывода, а затем
возбуждает прерывание. Драйвер сетевой карты принимает сигнал прерыва-
прерывания и выдает периферийному устройству команду на копирование пакета из
памяти ввода/вывода в буфер ядра. Когда пересылка данных закончится, се-
сетевая карта возбуждает еще одно прерывание, и драйвер уведомляет выше-
вышележащий уровень ядра о новом пакете.
Вспомогательные функции для передачи данных
посредством DMA
При создании драйвера устройства, использующего прямой доступ к памяти,
разработчик должен написать код, не зависящий от архитектуры и в то же
время не зависящий от типа шины в том, что касается DMA. Сейчас эта цель
достижима благодаря широкому набору вспомогательных функций DMA,
предлагаемых ядром. Эти функции прячут различия в механизмах DMA,
применяемых в разных архитектурах.
Существует два подмножества вспомогательных функций DMA. Старое со-
содержит архитектурно-независимые функции для PCI-устройств, а новое
обеспечивает независимость как от шины, так и от архитектуры. Далее мы
рассмотрим некоторые из этих функций и одновременно остановимся на не-
некоторых аппаратных особенностях схем DMA.
Адреса шины
При каждой пересылке данных посредством DMA задействован, как мини-
минимум, один буфер в памяти, содержащий данные, которые должно прочитать
или записать аппаратное устройство. В общем случае, перед активизацией
пересылки данных драйвер устройства должен обеспечить для схемы DMA
возможность прямого обращения к ячейкам оперативной памяти.
До этого момента мы различали три типа адресов памяти: логические и ли-
линейные адреса для "внутреннего пользования" центральным процессором и
физические адреса, т. е. адреса в памяти, используемые процессором для фи-
физического управления шиной данных. Есть и четвертый тип адресов памя-
памяти— так называемые адреса шины. Он соответствует адресам памяти, ис-
используемым всеми аппаратными устройствами, кроме процессора, для управ-
управления шиной данных.
Зачем же ядру понадобились адреса шины? Дело в том, что при прямом дос-
доступе к памяти пересылка данных происходит без вмешательства центрально-
центрального процессора. Шиной данных напрямую управляют устройства ввода/вывода
и схема DMA. Следовательно, когда ядро настраивает работу механизма
DMA, оно должно записать адрес шины задействованного буфера памяти в
соответствующие порты схемы DMA или устройства ввода/вывода.
В архитектуре 80x86 адреса шины совпадают с физическими адресами. Од-
Однако в других архитектурах, например SPARC фирмы Sun или Alpha фирмы
Hewlett-Packard, имеется электронная схема, называемая IO-MMU (I/O
Memory Management Unit, Блок управления памятью ввода/вывода). Это ана-
аналог блока управления страницами микропроцессора, отображающий физиче-
физические адреса в адреса шины. Все драйверы ввода/вывода, использующие схе-
схемы DMA, должны корректно настраивать IO-MMU перед началом пересылки
данных.
Размеры адреса различны у разных шин. Например, адрес шины у ISA имеет
размер 24 бита. Таким образом, в архитектуре 80x86 пересылки с использо-
использованием прямого доступа к памяти могут быть выполнены только в нижних
16 Мбайт физической памяти. Вот почему память для буфера, используемая в
этом случае, должна быть выделена в зоне zonedma с флагом gfpdma. Ориги-
Оригинальный стандарт PCI определяет 32-битовые адреса шин. Однако некоторые
PCI-устройства были первоначально сконструированы для шины ISA, и они
не могут обращаться к ячейкам памяти выше физического адреса OxOOffffff.
В новом стандарте PCI-X используются 64-битовые адреса шин, и схемы
DMA могут прямо обращаться к областям верхней памяти.
В Linux тип dmaaddrt представляет адрес шины. В архитектуре 80x86
dmaaddrt соответствует 32-битовому целому, если ядро не поддерживает
РАЕ (см. главу 2). При поддержке РАЕ dmaaddrt соответствует 64-битовому
целому.
Вспомогательные функции pci_set_dma_mask() И dmasetmaskO проверяют,
удовлетворяет ли шину данный размер адреса (маски), и, если это так, уве-
уведомляют, что данное периферийное устройство будет использовать адреса
этого размера.
Когерентность кэша
Архитектура системы не обязательно предлагает протокол когерентности
между аппаратным кэшем и схемами DMA на аппаратном уровне, поэтому
вспомогательные функции DMA должны принимать во внимание аппаратный
кэш при реализации операции отображения. Чтобы понять, почему это так,
предположим, что драйвер устройства заполняет буфер некоторыми данными
и затем немедленно выдает устройству команду на чтение этих данных с ис-
использованием прямого доступа к памяти. Если схема DMA обратится к физи-
физическим ячейкам оперативной памяти, но соответствующие строки аппаратно-
аппаратного кэша еще не будут записаны в оперативную память, то устройство прочи-
прочитает старое содержимое буфера памяти.
Разработчики драйверов могут обращаться с DMA-буферами двумя разными
способами, пользуясь двумя разными классами вспомогательных функций.
Употребляя терминологию Linux, можно сказать, что разработчик выбирает
между типами отображения прямого доступа к памяти:
□ когерентное отображение — когда задействован этот тип отображения,
ядро гарантирует отсутствие проблем когерентности кэша при взаимодей-
взаимодействии памяти и аппаратного устройства. Это означает, что каждая опера-
операция записи в память, выполненная центральным процессором, немедленно
становится видимой аппаратному устройству, и наоборот. Такой тип ото-
отображения еще называется синхронным, или согласованным;
□ потоковое отображение — при этом типе отображения драйвер устрой-
устройства должен позаботиться о когерентности кэша, используя вспомогатель-
вспомогательные функции синхронизации. Такой тип отображения называется также
асинхронным, или не когерентным.
При использовании прямого доступа к памяти в архитектуре 80x86 проблемы
когерентности кэша никогда не возникают. Дело в том, что устройства сами
следят за обращениями к аппаратным кэшам. Поэтому драйвер устройства,
разработанный специально под архитектуру 80x86, может выбрать любой из
двух типов DMA-отображения: здесь они равноценны. С другой стороны, во
многих архитектурах (например, MIPS, SPARC и в некоторых моделях
PowerPC) устройства не всегда отслеживают аппаратные кэши, и проблемы
когерентности имеют место. Вообще правильный выбор типа отображения
прямого доступа к памяти для архитектурно-независимого драйвера является
нетривиальной задачей.
В качестве общего правила, можно сказать, что, если центральный процессор
и процессор DMA обращаются к буферу непредсказуемым образом, следует
обязательно выбирать когерентное отображение (пример — буферы для ко-
командных структур адаптеров SCSI). В других случаях потоковое отображение
прямого доступа к памяти предпочтительнее, поскольку в некоторых архи-
архитектурах когерентное отображение громоздко и может понизить производи-
производительность системы.
Вспомогательные функции
для когерентного DMA-отображения
Обычно драйвер устройства выделяет буфер памяти и задает когерентное
DMA-отображение на этапе инициализации. Он освобождает отображение и
буфер, когда выгружается. Для выделения буфера и задания когерентного
отображения ядро предоставляет архитектурно-зависимые функции pci_aiioc_
consistent о и dmaaiioccoherent о. Обе они возвращают линейный адрес и
адрес шины нового буфера. В архитектуре 80x86 они возвращают линейный
и физический адреса нового буфера. Для освобождения отображения и буфе-
буфера ядро предоставляет функции pci_f ree_consistent () И dma_f ree_coherent ().
Вспомогательные функции
для потокового DMA-отображения
Буферы памяти для потоковых DMA-отображений обычно выделяются непо-
непосредственно перед пересылкой данных и освобождаются сразу после ее
окончания. Можно использовать одно отображение для нескольких пересы-
пересылок, но в этом случае разработчик драйвера должен учитывать наличие аппа-
аппаратного кэша между памятью и периферией.
Для организации потоковой DMA-пересылки драйвер должен вначале выде-
выделить буфер памяти с помощью зонного аллокатора страничных кадров или
общего аллокатора памяти (см. главу 8). Затем драйвер должен установить
потоковое DMA-отображение, вызвав либо функцию pcimapsingleo, либо
dmamapsingieo, которые принимают в качестве параметра линейный адрес
буфера и возвращают его адрес шины. Чтобы освободить отображение,
драйвер вызывает, соответственно, функцию pciunmapsingieO, либо
dma_unmap_single ().
Во избежание проблем когерентности, непосредственно перед началом пере-
пересылки данных из оперативной памяти устройству (с использованием меха-
механизма DMA) драйвер ДОЛЖен вызвать функцию pci_dma_sync_single_
for_device() ИЛИ функцию dma_sync_single_for_device (), которые В случае
необходимости очищают строки кэша, соответствующие буферу прямого
доступа к памяти. Аналогичным образом, драйверу устройства не следует
читать буфер памяти сразу по окончании пересылки данных от устройства в
оперативную память. Перед чтением буфера драйвер должен вызвать функ-
функцию pci_dma_sync_single_for_cpu() ИЛИ функцию dma_sync_single_for_cpu(),
которые, если нужно, очищают соответствующие строки аппаратного кэша.
В архитектуре 80x86 эти функции практически ничего не делают, поскольку
когерентность между аппаратными кэшами и схемами DMA поддерживается
на уровне аппаратуры.
Для пересылки данных с использованием прямого доступа к памяти годятся и
буферы в верхних областях памяти (см. главу 8). Разработчик должен вызвать
функцию pcimappage () ИЛИ функцию dmamappage (), передав В качестве
параметра дескриптор адреса страницы, содержащей буфер, и смещение бу-
буфера внутри страницы. Соответственно, чтобы освободить отображение бу-
буфера верхней памяти, разработчик вызывает функцию pciunmappage () или
dma_unmap_page ().
Уровни поддержки ядра
Ядро Linux не предоставляет полную поддержку всем существующим уст-
устройствам ввода/вывода. Вообще говоря, существует три возможных типа
поддержки аппаратного устройства:
□ отсутствие какой-либо поддержки— прикладная программа напрямую
взаимодействует с портами ввода/вывода данного устройства, используя
команды ассемблера in и out;
□ минимальная поддержка — ядро не распознает устройство, но распознает
его интерфейс ввода/вывода. Пользовательские программы способны вос-
воспринимать интерфейс как последовательное устройство, которое читает
и/или пишет последовательности символов;
□ расширенная поддержка — ядро распознает аппаратное устройство и само
работает с его интерфейсом ввода/вывода. Фактически, даже существова-
существование файла устройства не является обязательным.
Самым распространенным примером первого подхода, при котором не ис-
используется никакой драйвер ядра, является традиционное управление графи-
графическим дисплеем в системе X Window. Оно очень эффективно, хотя не дает
Х-серверу использовать аппаратные прерывания, вызываемые устройством
ввода/вывода. При этом подходе требуется приложить некоторые дополни-
дополнительные усилия, чтобы позволить Х-серверу обращаться к нужным портам
ввода/вывода. Как было сказано в главе 3, системные вызовы iopi () и
iopermo дают процессу привилегию обращения к портам ввода/вывода. Де-
Делать эти вызовы могут только программы с привилегиями root, но эти про-
программы можно сделать доступными простому пользователю, установив флаг
seuid у выполняемого файла (см. разд. "Права и возможности процесса" гла-
главы 20).
Последние версии Linux поддерживают несколько широко распространенных
типов графических карт. Файл устройства /dev/fb является абстракцией
фрейм-буфера графической карты и позволяет прикладным программам об-
обращаться к нему, не требуя от них знания портов ввода/вывода графического
интерфейса. Кроме того, ядро поддерживает DRI (Direct Rendering
Infrastructure, Инфраструктура прямого рендеринга), давая прикладным про-
программам возможность работать с аппаратными ускорителями трехмерной
графики. В любом случае, традиционной сервер системы X Window типа
"все-в-одном" по-прежнему весьма популярен.
Подход с оказанием минимальной поддержки применяется для управления
внешними устройствами, подключенными к интерфейсам ввода/вывода об-
общего назначения. Ядро обеспечивает интерфейс ввода/вывода, предоставляя
файл устройства (и тем самым драйвер устройства), а прикладная программа
управляет внешним устройством, читая содержимое файла устройства и за-
записывая в него данные.
Минимальная поддержка предпочтительнее расширенной, потому что она
позволяет иметь ядро небольшого размера. Однако среди интерфейсов вво-
ввода/вывода общего назначения, типичных для архитектуры PC, только после-
последовательный и параллельный порты позволяют применять к ним этот подход.
Например, мышь напрямую управляется прикладной программой, такой как
Х-сервер, а последовательному модему требуется коммуникационная про-
программа, такая как Minicom, Seyon или демон протокола РРР (Point-to-Point
Protocol, Протокол передачи типа "точка-точка").
Минимальная поддержка имеет ограниченный круг применения, потому что
она не годится в тех случаях, когда внешнее устройство должно активно
взаимодействовать с внутренними структурами данных ядра. Рассмотрим,
например, съемный жесткий диск, подключенный к интерфейсу ввода/вывода
общего назначения. Прикладная программа не может обращаться ко всем
внутренним структурам ядра и нуждается в специальных функциях для рас-
распознавания диска и монтирования его файловой системы. В такой ситуации
требуется расширенная поддержка устройства.
В общем случае каждому аппаратному устройству, напрямую подключенно-
подключенному к шине ввода/вывода (пример — внутренний жесткий диск), оказывается
расширенная поддержка. Иными словами, ядро должно предоставить драйвер
для каждого такого устройства. Внешние устройства, подключенные к USB,
к порту PCMCIA, имеющемуся у многих ноутбуков, или к SCSI-интер-
SCSI-интерфейсу — одним словом, к любому интерфейсу ввода/вывода, кроме последо-
последовательного и параллельного портов — требуют расширенной поддержки.
Следует заметить, что стандартные системные вызовы, относящиеся к работе
с файлами, такие как open (), read () и write (), не всегда обеспечивают при-
приложению полный контроль над соответствующим аппаратным устройством.
На самом деле, VFS придерживается принципа минимальной необходимости
и не предоставляет специальных команд, необходимых некоторым устройст-
устройствам, а также не позволяет приложению проверять, находится ли устройство
в каком-то конкретном состоянии.
Системный вызов ioctio был введен специально для удовлетворения таких
потребностей. Помимо дескриптора файла устройства и 32-битового второго
параметра, уточняющего запрос, этот системный вызов может принимать
произвольное количество дополнительных параметров. Например, сущест-
существуют специализированные запросы ioctio, позволяющие узнать громкость
воспроизведения компакт-диска или выдвигать лоток. С помощью этих за-
запросов прикладные программы могут обеспечить пользовательский интер-
интерфейс к CD-raieepy.
Драйверы символьных устройств
Управление символьным устройством является относительно простой зада-
задачей, поскольку не включает в себя сложные стратегии буферизации и не ис-
использует дисковые кэши. Конечно, символьные устройства различаются ме-
между собой. Одни требуют реализации сложных коммуникационных протоко-
протоколов, в то время как другие просто считывают несколько значений с пары
портов ввода/вывода. Например, драйвер карты с несколькими последова-
последовательными портам устроен гораздо сложнее драйвера мыши.
Зато драйверы блочных устройств по сути своей сложнее, чем драйверы сим-
символьных. Приложения нередко многократно читают или записывают один и
тот же блок данных. Кроме того, доступ к таким устройствам, как правило,
требует много времени. Эти особенности оказывают сильное влияние на
структуру драйверов. Однако, как мы увидим в следующих главах, для рабо-
работы с такими устройствами ядро предоставляет необходимые компоненты,
такие как кэш страниц и подсистема блочного ввода/вывода. В последних
разделах этой главы мы сконцентрируем внимание на драйверах символьных
устройств.
Драйвер символьного устройства описывается структурой cdev, поля которой
перечислены в табл. 13.8.
Таблица 13.8. Поля структуры cdev
Тип Поле Описание
struct kobject * kobj Встроенный объект kobject
struct module * owner Указатель на модуль, реализующий драйвер, если
таковой имеется
struct f ile_operations * ops Указатели на таблицу файловых операций
драйвера
struct list_head list Голова списка индексных дескрипторов, имеющих
отношение к файлам данного символьного
устройства
dev_t dev Исходные старший и младший номера,
присвоенные драйверу устройства
unsigned int count Размер диапазона номеров устройств,
имеющегося в распоряжении драйвера
Поле list является головой двунаправленного циклического списка индекс-
индексных дескрипторов файлов символьного устройства, которые имеют отноше-
отношение к этому драйверу. Возможно наличие большого количества файлов уст-
устройства, имеющих один и тот же номер устройства и, таким образом, отно-
относящихся к одному символьному устройству. Кроме того, драйвер устройства
может быть ассоциирован не с одним номером устройства, а с целым диапа-
диапазоном номеров. Все файлы устройств, номера которых попадают в этот диа-
диапазон, управляются одним драйвером. Размер диапазона хранится в поле
count.
Функция cdevaiioc () динамически выделяет дескриптор cdev и инициализи-
инициализирует встроенный объект kobject так, чтобы дескриптор автоматически осво-
освобождался, когда счетчик ссылок дойдет до нуля.
Функция cdevaddo регистрирует дескриптор cdev в модели драйвера уст-
устройства. Она инициализирует поля dev и count дескриптора cdev, а затем вы-
вызывает функцию kobj map (), которая соответствующим образом настраивает
структуры модели драйвера устройства, связывающие интервал номеров уст-
устройств с данным дескриптором драйвера.
В модели драйвера устройства определена область отображения объектов
kobject для символьных устройств, которая представлена дескриптором типа
kobj map и на которую ссылается глобальная переменная cdevmap. Дескрип-
Дескриптор kobj map содержит хеш-таблицу из 255 записей, индексированную по
старшим номерам интервалов. В хеш-таблице хранятся объекты типа probe,
по одному на каждый зарегистрированный диапазон старших и младших но-
номеров. Поля одного такого объекта перечислены в табл. 13.9.
Таблица 13.9. Поля объекта probe
Тип Поле Описание
struct probe * next Следующий элемент в списке хеш-коллизий
devt dev Исходный номер устройства (старший и младший)
из интервала
unsigned long range Размер интервала
struct module * owner Указатель на модуль, реализующий драйвер, если
таковой имеется
struct kobject *(*) get Метод для определения владельца интервала
(dev_t, int *, void *)
int (*) (dev_t, void *) lock Метод для увеличения счетчика ссылок владельца
интервала
void * data Закрытые данные владельца интервала
Когда вызывается функция kobjmapo, указанный интервал номеров уст-
устройств добавляется в хеш-таблицу. Поле data соответствующего объекта
probe указывает на дескриптор cdev драйвера устройства. Значение этого по-
поля передается методам get и lock, когда они вызываются. В этом случае ме-
метод get реализуется короткой функцией, которая возвращает адрес объекта
kobject, встроенного в дескриптор cdev. Что касается метода lock, он увели-
увеличивает счетчик ссылок во встроенном объекте kobject.
Функция kobjiookupO принимает в качестве входных параметров область
отображения объектов kobject и номер устройства. Она выполняет поиск в
хеш-таблице и возвращает адрес объекта kobject драйвера-владельца интер-
интервала, включающего в себя указанный номер, если такой имеется. Когда речь
идет об области отображения, определенной для символьных устройств,
функция возвращает адрес объекта kobject, встроенного в дескриптор cdev
драйвера-владельца интервала номеров устройств.
Присваивание номеров устройств
Для отслеживания номеров, присвоенных символьным устройствам, ядро ве-
ведет хеш-таблицу chrdevs, в которой хранятся интервалы номеров устройств.
Два интервала могут иметь один и тот же старший номер, но они не могут
пересекаться, и поэтому их младшие номера должны быть разными. Таблица
рассчитана на 255 записей, и хеш-функция маскирует четыре старших бита
старшего номера. Таким образом старшие номера, меньшие 255, хешируются
в разные записи. Каждая запись указывает на первый элемент списка колли-
коллизий, упорядоченного по возрастанию старших и младших номеров.
Каждый элемент списка является структурой chardevicestruct, поля кото-
которой представлены в табл. 13.10.
Таблица 13.10. Поля дескриптора char_device_struct
Тип Поле Описание
unsigned next Указатель на следующий элемент в списке
char_device_struct * хеш-коллизий
unsigned int major Старший номер из интервала
unsigned int baseminor Первый младший номер из интервала
int minorct Размер интервала
const char * name Имя драйвера устройства, работающего
с данным интервалом
struct file_ope rat ions * fops He используется
struct cdev * cdev Указатель на дескриптор драйвера
символьного устройства
Существует два метода для присваивания диапазона номеров устройств
драйверу символьного устройства. Первый, который следует использовать
ДЛЯ ВСеХ НОВЫХ Драйверов, ОСНОВан На фуНКЦИЯХ register_chrdev_region() И
aiioc_chrdev_region(). Он заключается в присваивании драйверу произволь-
произвольного диапазона номеров устройств. Например, чтобы получить интервал но-
номеров, начинающийся со значения dev типа devt и имеющий ширину size,
нужно написать:
register_chrdev_region(dev, size, "foo");
Эти функции не вызывают функцию cdevadd (), так что драйвер устройства
должен вызвать ее сам после успешного получения запрошенного интервала.
Второй метод основан на функции registerchrdevo и состоит в присваива-
присваивании фиксированного интервала номеров устройств, включающего в себя один
старший номер и младшие номера от 0 до 255. В этом случае драйвер не дол-
должен вызывать cdevadd ().
Функции register_chrdev_region() и alloc_chrdev_region()
Функция register_chrdev_region() принимает три параметра: исходный но-
номер устройства (старший и младший номера), размер запрошенного диапазо-
диапазона номеров (то есть количество требуемых младших номеров) и имя драйвера
устройства, который запросил диапазон номеров. Функция проверяет, захва-
захватывает ли запрошенный диапазон несколько старших номеров, и, если это так,
определяет старшие номера и соответствующие интервалы, покрывающие
весь Диапазон. Затем фунКЦИЯ ВЫЗЫВает фуНКЦИЮ register_chrdev_region()
(описанную далее) для каждого из этих интервалов.
Функция aiiocchrdevregiono аналогична предыдущей, но служит для ди-
динамического выделения старших номеров. В качестве параметров она прини-
принимает исходный младший номер из интервала, размер интервала и имя драй-
драйвера устройства. Эта функция тоже вызывает register_chrdev_region ().
ФунКЦИЯ registerchrdevregion () ВЫПОЛНЯет следующие деЙСТВИЯ!
1. Выделяет новую структуру chardevicestruct и заполняет ее нулями.
2. Если старший номер из интервала равен нулю, значит, драйвер запросил
динамическое выделение старшего номера. Начиная с последней записи в
хеш-таблице и следуя в обратном направлении, функция ищет пустой спи-
список коллизий (указатель null), который соответствует еще не задейство-
задействованному старшему номеру. Если пустая запись не найдена, функция воз-
возвращает код ошибки6.
3. Инициализирует Соответствующие ПОЛЯ Структуры chardevicestruct
первым номером устройства из интервала, размером интервала и именем
драйвера.
4. Вызывает хеш-функцию для вычисления индекса в хеш-таблице по стар-
старшему номеру.
5. Проходит по списку коллизий в поисках корректной позиции для новой
структуры chardevicestruct. Если по ходу дела обнаруживается интер-
интервал, перекрывающийся запрошенным, функция возвращает код ошибки.
6. Вставляет НОВЫЙ дескриптор chardevicestruct В СПИСОК КОЛЛИЗИЙ.
7. Возвращает адрес нового дескриптора chardevicestruct.
Функция register_chrdev()
Функция registerchrdevo используется драйверами, которым требуется
интервал номеров, так сказать, в "старом стиле": один старший номер и
младшие номера от 0 до 255. В качестве параметров функция принимает
старший номер major (ноль для динамического выделения), имя драйвера
устройства name и указатель fops на таблицу файловых операций, специфич-
специфичных для файлов символьного устройства с номерами из данного интервала.
6 Обратите внимание, что ядро может динамически выделять только старшие номера, меньшие 255,
и что в некоторых случаях выделение может закончиться неудачей, даже если имеется старший
номер, меньший 255. Остается надеяться, что эти ограничения будут сняты в будущем.
Функция registerchrdev () выполняет следующие действия:
1. Вызывает функцию register_chrdev_region() для выделения запрошен-
ного интервала. Если вызванная функция возвращает код ошибки (интер-
(интервал не может быть присвоен), описываемая функция завершает работу.
2. Выделяет новую структуру cdev для драйвера устройства.
3. Инициализирует структуру cdev:
• устанавливает тип встроенного объекта kobject равным ktype_cdev_
dynamic;
• записывает в поле owner содержимое переменной fops->owner;
• записывает в поле ops адрес fops таблицы файловых операций;
• копирует символы, образующие имя драйвера в поле name встроенного
kobject.
4. Вызывает функцию cdev_add ().
5. Записывает в поле cdev дескриптора chardevicestruct, возвращенного
функцией registerchrdevregion () на шаге 1, адрес дескриптора драй-
драйвера cdev.
6. Возвращает старший номер присвоенного интервала.
Обращение к драйверу символьного устройства
Ранее мы отмечали, что функция dentryopeno, вызываемая из служебной
процедуры системного вызова open (), настраивает поле f_ор файлового объ-
объекта символьного устройства так, чтобы оно указывало на таблицу def_
chrfops. Эта таблица почти пуста. Она лишь определяет функцию
chrdevopen() как метод ореп() файла устройства. Этот метод вызывается
функцией dentry_open ().
Функция chrdevopen () принимает в качестве параметров адреса inode и filp
индексного дескриптора и файлового объекта, относящихся к открываемому
файлу устройства. Она выполняет следующие операции:
1. Проверяет указатель inode->i_cdev на дескриптор cdev драйвера устройст-
устройства. Если это поле не равно null, значит, к индексному дескриптору уже
обращались. Функция увеличивает счетчик ссылок дескриптора cdev и пе-
переходит к шагу 6.
2. Вызывает функцию kobj lookup о, чтобы найти интервал, включающий в
себя номер устройства. Если такого интервала нет, возвращает код ошиб-
ошибки; в противном случае вычисляет адрес дескриптора cdev, ассоциирован-
ассоциированного с интервалом.
3. Записывает в поле inode->i_cdev индексного дескриптора адрес дескрип-
дескриптора cdev.
4. Записывает в поле inode->i_cindex относительный индекс номера устрой-
устройства в пределах интервала, присвоенного драйверу устройства (нулевой
индекс — для первого младшего номера в интервале, единица — для вто-
второго и т. д.).
5. Заносит индексный дескриптор в список, на который указывает поле list
дескриптора cdev.
6. Инициализирует указатель на файловые операции fiip->f_ops содержи-
содержимым ПОЛЯ ops Дескриптора cdev.
7. Если метод fiip->f_ops->open определен, функция выполняет его. Если
драйвер работает более чем с одним номером устройства, эта функция, как
правило, снова настраивает операции файлового объекта, чтобы были ус-
установлены файловые операции, подходящие для работы с данным файлом
устройства.
8. Заканчивает свою работу, возвращая ноль (удачное завершение).
Стратегии буферизации
для символьных устройств
По традиции Unix-подобные операционные системы делят аппаратные уст-
устройства на блочные и символьные. Однако такая классификация не дает пол-
полной картины. Некоторые устройства способны передавать большие объемы
данных за одну операцию ввода/вывода, в то время как другие передают
лишь несколько символов.
Например, драйвер мыши PS/2 за каждую операцию чтения получает не-
несколько байтов, в которых содержится информация о состоянии кнопки мы-
мыши и о позиции указателя мыши на экране. Таким устройством управлять
проще всего. Вначале входные данные посимвольно считываются с входного
регистра и сохраняются в соответствующей структуре ядра. Затем эти данные
в подходящий момент времени копируются в адресное пространство процес-
процесса. Аналогичным образом, выходные данные вначале копируются из адресно-
адресного пространства процесса в соответствующую структуру ядра, а затем посим-
посимвольно копируются в выходной регистр устройства ввода/вывода. Очевидно,
что драйверы ввода/вывода для таких устройств не пользуются механизмом
DMA, поскольку процессорное время, затраченное на настройку операций
ввода/вывода с прямым доступом к памяти, сравнимо со временем, необхо-
необходимым для пересылки данных на порты ввода/вывода или обратно.
С другой стороны, ядро должно быть также готово к работе с устройствами,
перемещающими большое количество байтов за каждую операцию вво-
да/вывода. Это могут быть либо последовательные устройства, такие как зву-
звуковые или сетевые карты, либо устройства с произвольным доступом, напри-
например, все виды дисковых приводов (гибкие, CD-ROM, SCSI и т. д.).
Предположим, вы настроили звуковую карту на компьютере так, что можете
записывать звуки с помощью микрофона. Звуковая карта оцифровывает элек-
электрический сигнал, поступающий с микрофона, с фиксированной частотой,
скажем, 44,14 кГц. В результате она выдает поток 16-битовых чисел, разде-
разделенный на блоки входных данных. Драйвер звуковой карты должен уметь
справляться с такой лавиной информации в любой ситуации, даже когда цен-
центральный процессор занят выполнением другого процесса.
Этого можно добиться, сочетая две разные методики:
□ использование для передачи блоков данных механизма DMA;
□ использование циклического буфера из двух и более элементов, каждый
из которых имеет размер, равный блоку данных. Когда возникнет преры-
прерывание, сигнализирующее, что прочитан новый блок данных, обработчик
прерывания передвинет указатель на элемент циклического буфера так,
чтобы следующие данные записывались в пустой элемент. И наоборот, как
только драйвер успешно скопирует блок данных в адресное пространство
пользователя, он освобождает элемент циклического буфера, чтобы он
был доступен для сохранения новых данных от аппаратного устройства.
Задача циклического буфера заключается в сглаживании пиков загрузки цен-
центрального процессора. Даже если приложение режима пользователя, прини-
принимающее данные, замедлит свою работу из-за наличия более приоритетных
задач, схема DMA сможет продолжать заполнять элементы циклического
буфера, потому что обработчик прерываний работает за счет текущего
процесса.
Сходная ситуация возникает при приеме пакетов от сетевой карты, но в этом
случае поток поступающих данных является асинхронным. Пакеты прини-
принимаются независимо друг от друга, и временной интервал между приходом
двух пакетов непредсказуем.
Учитывая все сказанное, работа с буфером для последовательных устройств
не представляет трудности, поскольку один и тот же буфер никогда не ис-
используется повторно. В самом деле, не может же аудиоприложение попро-
попросить микрофон снова передать предыдущий блок данных.
Мы увидим в главе 75, что буферизация для устройства с произвольным дос-
доступом (все виды дисковых накопителей) является гораздо более сложной за-
задачей.
ГЛАВА 14
Драйверы блочных устройств
В этой главе описываются драйверы блочных устройств, т. е. диски всевоз-
всевозможных типов. Важнейшей особенностью блочного устройства является не-
несоответствие между временем, необходимым процессору и шинам для чтения
и записи данных, и скоростью работы механизмов накопителя. У блочных
устройств среднее время доступа к данным исключительно велико. На каж-
каждую операцию уходит несколько миллисекунд, в основном, из-за того, что
контроллер диска должен перемещать головки над поверхностью диска в то
место, где находятся данные. Впрочем, когда головки расположены как надо,
чтение или запись данных может происходить со скоростью до нескольких
десятков мегабайт в секунду.
Организация управления блочными устройствами в Linux чрезвычайно слож-
сложна. Мы не сможем подробно обсудить все функции подсистемы блочного
ввода/вывода, имеющиеся в ядре, но мы все-таки дадим общее описание про-
программной архитектуры. Как и в предыдущей главе, наша цель состоит в том,
чтобы объяснить читателю, как Linux поддерживает реализацию драйверов
блочных устройств, а не в показе реализации какого-то конкретного драй-
драйвера.
Мы начнем с разд. "Управление блочными устройствами", где опишем об-
общую архитектуру подсистемы блочного ввода/вывода в Linux. В разд. "Об-
"Общий слой работы с блочными устройствами", "Планировщик ввода/вывода"
и "Драйверы блочных устройств" мы опишем основные компоненты подсис-
подсистемы блочного ввода/вывода. В заключительном разделе, "Открытие файла
блочного устройства", мы в общих чертах опишем шаги, выполняемые
ядром при открытии файла блочного устройства.
Управление блочными устройствами
В каждой операции с драйвером блочного устройства задействовано множе-
множество компонентов ядра. Самые важные из них представлены на рис. 14.1.
Рис. 14.1. Компоненты ядра,
задействованные в операции с блочным устройством
Предположим, например, что процесс сделал системный вызов read о для
некоторого файла на диске (мы вскоре убедимся, что запросы на запись обра-
обрабатываются практически так же). Как правило, чтобы обслужить запрос про-
процесса, ядро предпринимает следующие шаги:
1. Служебная процедура системного вызова read о активизирует соответст-
соответствующую функцию VFS, передавая ей дескриптор файла и смещение внут-
внутри файла. Виртуальная файловая система является верхним слоем в архи-
архитектуре управления блочными устройствами. Она предоставляет общую
файловую модель, принятую всеми файловыми системами, поддерживае-
поддерживаемыми в Linux. Мы подробно описали слой виртуальной файловой системы
в главе 12.
2. Функция виртуальной файловой системы определяет, доступны ли запро-
запрошенные данные, и, если потребуется, как именно должна быть выполнена
операция чтения. Иногда нет необходимости в обращении к диску, потому
что ядро хранит в оперативной памяти данные, недавно прочитанные с
блочного устройства (или записанные на него). Принцип работы кэша
диска описан в главе 75, а подробности того, как виртуальная файловая
система выполняет дисковые операции и взаимодействует с кэшем диска и
реальными файловыми системами, приведены в главе 16.
Предположим, что ядро должно прочитать данные с блочного устройства,
т. е. оно должно определить физическое местонахождение данных. Эту зада-
задачу оно решает с помощью слоя отображения, который в типичном случае
выполняет следующие действия:
□ он определяет размер блока файловой системы, содержащей данный файл,
и вычисляет объем запрошенных данных в терминах номеров блоков фай-
файла. Файл считается разбитым на множество блоков, и ядро выясняет номе-
номера (индексы относительно начала файла) блоков, содержащих запрошен-
запрошенные данные;
□ затем слой отображения вызывает специфичную для файловой системы
функцию, которая читает индексный дескриптор файла и определяет по-
позицию запрошенных данных на диске в терминах номеров логических
блоков. Диск считается разбитым на множество блоков, и ядро выясняет
номера (индексы относительно начала диска или раздела) блоков, содер-
содержащих запрошенные данные. Поскольку файл может храниться не в
смежных блоках диска, структура, хранящаяся в индексном дескрипторе,
отображает номер каждого блока файла в номер логического блока1.
Работу слоя отображения мы увидим в главе 16, а некоторые распростра-
распространенные дисковые файловые системы представлены в главе 18.
Теперь ядро может выполнять операцию чтения с блочного устройства. Для
этого оно использует общий слой работы с блочными устройствами, который
запускает операции ввода/вывода для пересылки запрошенных данных. Во-
Вообще, каждая операция ввода/вывода затрагивает группу смежных блоков на
диске. Поскольку запрошенные данные необязательно хранятся в соседних
блоках, общий слой работы с блочными устройствами, возможно, будет дол-
должен запустить несколько операций ввода/вывода. Каждая такая операция
представлена структурой bio (сокр. от "block I/O", блочный ввод/вывод), ко-
которая собирает всю информацию, необходимую нижним слоям для удовле-
удовлетворения запроса.
1 Однако, если запрос на чтение был сделан для "чистого" файла блочного устройства, слой ото-
отображения не станет вызывать специфичный для файловой системы метод. Вместо этого он преобра-
преобразует смещение в файле блочного устройства в позицию на диске (или разделе диска) соответственно
файлу устройства.
Общий слой работы с блочными устройствами скрывает особенности любого
аппаратного блочного устройства, предлагая абстрактный взгляд на блочные
устройства. Поскольку почти все они являются дисками, общий слой работы
с блочными устройствами содержит некоторые общие структуры данных,
описывающие диски и разделы на дисках. Мы обсудим этот слой и структуру
bio в разд. "Общий слой работы с блочными устройствами" далее в этой
главе.
Под общим слоем работы с блочными устройствами расположен планиров-
планировщик ввода/вывода. Он сортирует невыполненные запросы на пересылку дан-
данных в соответствии с заранее определенной политикой ядра. Целью плани-
планировщика является группирование запросов по принципу близости располо-
расположения запрошенных данных на физическом носителе. Мы опишем этот слой
в разд. "Планировщик ввода/вывода" далее в этой главе.
Наконец, драйверы блочных устройств берут на себя работу по фактической
пересылке данных, отдавая команды аппаратным интерфейсам контроллеров
диска. Общая организация типичного драйвера блочного устройства разъяс-
разъясняется в разд. "Драйверы блочных устройств" далее в этой главе.
Как видите, существует множество компонентов ядра, имеющих дело с дан-
данными, хранящимися в блочных устройствах, и каждый из них управляет пор-
порциями данных определенной длины.
□ Контроллеры аппаратных блочных устройств передают данные порциями
фиксированной длины, называемыми секторами. То есть планировщик
ввода/вывода и драйверы блочных устройств должны оперировать секто-
секторами данных.
□ Виртуальная файловая система, слой отображения и файловые системы
группируют данные на диске в логические единицы, называемые блоками.
Блок соответствует минимальной единице памяти на диске в данной фай-
файловой системе.
□ Как мы вскоре убедимся, драйверы блочных устройств должны уметь об-
обращаться с "сегментами" данных. Каждый сегмент является страницей па-
памяти или частью страницы и включает в себя порции данных, располо-
расположенные на диске по соседству.
□ Кэши диска работают со "страницами", каждая из которых совпадает по
размеру со страничным кадром.
□ Общий слой работы с блочными устройствами "склеивает" все верхние и
нижние компоненты. Таким образом, он знает и о секторах, и о блоках, и о
сегментах, и о страницах.
Даже при наличии большого количества разных порций данных они распола-
располагаются в одних и тех же ячейках оперативной памяти. Например, на рис. 14.2
показана схема 4096-байтовой страницы. Верхние компоненты ядра воспри-
воспринимают страницу как состоящую из четырех буферов по 1024 байтов. По-
Последние три блоки страницы передаются драйвером блочного устройства и
записываются в сегмент, занимающий последние 3072 байта страницы. Кон-
Контроллер жесткого диска считает, что сегмент состоит из шести секторов по
512 байтов.
Рис. 14.2. Типичная схема страницы, содержащей данные с диска
В этой главе мы опишем нижние компоненты ядра, управляющие блочными
устройствами: общий слой работы с блочными устройствами, планировщик
ввода/вывода и драйверы устройств. При этом мы, естественно, коснемся
секторов, блоков и сегментов.
Секторы
Для достижения приемлемой производительности жесткие диски и аналогич-
аналогичные устройства передают несколько соседних байтов одновременно. Каждая
операция пересылки данных, выполняемая для блочного устройства, дейст-
действует на группу смежных байтов, называемую сектором. В дальнейшем мы
будем называть группы байтов смежными, если они записаны на поверхности
диска так, что к ним можно обратиться за одну операцию поиска. Хотя физи-
физическая геометрия диска обычно очень сложна, контроллер жесткого диска
воспринимает команды, трактующие диск как большой массив секторов.
У большинства дисков размер сектора равен 512 байтам, но есть и устройства
с более крупными секторами A024 и 2048 байтов). Обратите внимание, что к
сектору следует относиться как к базовой единице пересылки данных; невоз-
можно переслать менее одного сектора, хотя большинство дисковых уст-
устройств способно передавать несколько смежных секторов за одну операцию.
В Linux размер сектора по соглашению составляет 512 байтов. Если блочное
устройство работает с более крупными секторами, то драйвер более низкого
уровня выполнит необходимые преобразования. Таким образом, группа дан-
данных, хранящихся на блочном устройстве, идентифицируется своей позицией
на диске — индексом своего первого 512-байтового сектора, а ее длина опре-
определяется количеством 512-байтовых секторов. Индексы секторов хранятся
в 32- или 64-битовых переменных типа sectort.
Блоки
В то время как сектор является базовой единицей пересылки данных для ап-
аппаратных устройств, блок представляет собой базовую единицу для VFS и,
следовательно, для файловых систем. Например, когда ядро обращается к
содержимому файла, оно вначале должно прочитать с диска блок, содержа-
содержащий индексный дескриптор файла (см. главу 12). Этот блок на диске соответ-
соответствует одному или нескольким смежным секторам, которые воспринимаются
виртуальной файловой системой как одна единица данных.
В Linux размер блока должен быть степенью двойки, и он не должен превы-
превышать размер страничного кадра. Более того, он должен быть кратным размеру
сектора, потому что каждый блок должен содержать целое количество секто-
секторов. Следовательно, в архитектуре 80x86 разрешены размеры блоков 512,
1024, 2048 и 4096 байтов.
Размер блока не является специфичным для блочного устройства. При созда-
создании дисковой файловой системы администратор может выбрать подходящий
размер блока. Так, несколько разделов на одном диске могут использовать
блоки разных размеров. Кроме того, каждая операция чтения или записи, вы-
выполненная напрямую над файлом блочного устройства, обращается непо-
непосредственно к устройству, в обход дисковой файловой системы. Ядро выпол-
выполняет ее, используя блоки максимального размера D096 байтов).
Каждому блоку нужен собственный буфер. Это область оперативной памяти,
в которой ядро сохраняет содержимое блока. Когда ядро считывает блок с
диска, оно заполняет соответствующий буфер блока данными, полученными
от аппаратного устройства. Аналогичным образом, когда ядро записывает
блок на диск, оно обновляет содержимое группы смежных байтов в аппарат-
аппаратном устройстве значениями, полученными из буферами блока. Размер буфера
блока всегда равен размеру самого блока.
У каждого буфера есть дескриптор "головы буфера", имеющий тип
buf ferhead. Этот дескриптор содержит всю информацию, необходимую яд-
ру, чтобы знать, как обращаться с данным буфером. Таким образом, перед
работой с каждым буфером ядро проверяет его "голову". Подробное описа-
описание всех полей буфера приведено в главе 15. Здесь же мы рассмотрим лишь
несколько полей: b_page, b_data, b_blocknr И b_bdev.
Поле Ьраде содержит адрес дескриптора страницы для страничного кадра, в
котором хранится буфер блока. Если страничный кадр находится в верхней
памяти, поле bdata содержит смещение буфера блока внутри страницы.
В противном случае оно содержит начальный линейный адрес самого буфера
блока. В поле bbiocknr хранится номер логического блока (то есть индекс
блока в разделе диска). Наконец, поле bbdev идентифицирует блочное уст-
устройство, использующее голову буфера (см. разд. "Блочные устройства" да-
далее в этой главе).
Сегменты
Мы знаем, что каждая дисковая операция ввода/вывода состоит из пересылки
содержимого одного или нескольких смежных секторов в некоторые ячейки
оперативной памяти (или из них). Почти во всех случаях пересылка данных
выполняется непосредственно контроллером диска с использованием меха-
механизма DMA (см. главу 13). Драйвер блочного устройства просто инициирует
пересылку данных, отправляя контроллеру диска необходимые команды. По
окончании пересылки контроллер возбуждает прерывание, чтобы уведомить
драйвер.
Данные, пересланные за одну операцию прямого доступа к памяти, должны
принадлежать смежным секторам диска. Это физическое требование: кон-
контроллер диска, который позволял бы пересылать данные из несмежных сек-
секторов, имел бы очень низкую скорость передачи, поскольку перемещение
читающей/пишущей головки над поверхностью диска происходит довольно
медленно.
Старые контроллеры дисков поддерживают только "простые" DMA-
операции: в каждой такой операции данные пересылаются в физически не-
непрерывные участки оперативной памяти (или из них). Зато современные кон-
контроллеры могут поддерживать так называемые пересылки вразброс с исполь-
использованием DMA. В каждой такой операции данные можно пересылать с уча-
участием нескольких не смежных областей памяти.
При каждой пересылке вразброс драйвер блочного устройства должен отпра-
отправить контроллеру диска:
□ номер первого сектора и общее количество пересылаемых секторов;
□ список дескрипторов областей памяти, каждый из которых состоит из ад-
адреса и длины.
Контроллер диска сам выполняет всю пересылку данных. Например, при
операции чтения он получает данные из смежных секторов диска и "разбра-
"разбрасывает" их по различным областям памяти.
Чтобы воспользоваться DMA-пересылками вразброс, драйверы блочных уст-
устройств должны обрабатывать данные, организованные в единицы, называе-
называемые сегментами. Сегмент — это просто страница памяти или ее часть, кото-
которая содержит данные из нескольких смежных секторов диска. Так в операции
вразброс могут участвовать сразу несколько сегментов.
Обратите внимание, что драйверу блочного устройства ничего не нужно
знать о блоках, их размерах и буферах. Даже если сегмент виден на более вы-
высоком уровне как страница, состоящая из нескольких буферов блоков, драй-
драйверу все равно.
Как мы увидим далее, общий слой работы с блочными устройствами может
объединять разные сегменты, если окажется, что соответствующие странич-
страничные кадры непрерывны в оперативной памяти, а порции данных на диске
смежны друг с другом. Более крупная область памяти, являющаяся результа-
результатом этого объединения, называется физическим сегментом.
Другая операция слияния возможна в архитектурах, которые выполняют ото-
отображение между адресами шины и физическими адресами при помощи спе-
специальной электронной схемы на шине (IO-MMU, см. главу 13). Область па-
памяти, получающаяся в результате такого объединения, называется аппарат-
аппаратным сегментом. Поскольку мы уделяем основное внимание архитектуре
80x86, не поддерживающей динамическое отображение между адресами ши-
шины и физическими адресами, далее в этой главе мы будем предполагать, что
аппаратные сегменты всегда совпадают с физическими.
Общий слой работы
с блочными устройствами
Общий слой работы с блочными устройствами является компонентом ядра,
который обрабатывает запросы всех блочных устройств в системе. Благодаря
его функциям, ядро может:
□ помещать буферы данных в верхнюю память — страничные кадры будут
отображаться в линейное адресное пространство ядра только тогда, когда
центральному процессору нужно обратиться к данным, а сразу после этого
отображение будет отменено;
□ реализовать (с некоторыми дополнительными усилиями) схему "нуль-
копирования", при которой данные с диска помещаются прямо в адресное
пространство режима пользователя без предварительного копирования в
память ядра. Фактически буфер, используемый ядром, для ввода/вывода
находится на страничном кадре, который отображается в линейное адрес-
адресное пространство режима пользователя, принадлежащее некоторому про-
процессу;
□ управлять логическими томами, например, такими, которые используются
в LVM (Logical Volume Manager, Менеджер логических томов) и RAID
(Redundant Array of Inexpensive Disks, Избыточный массив недорогих дис-
дисков), несколько разделов на диске или даже на разных блочных устройст-
устройствах видны как один раздел;
□ задействовать передовые функции, предлагаемые самыми современными
контроллерами дисков — большие встроенные кэши, улучшенный меха-
механизм DMA, аппаратное планирование запросов ввода/вывода и т. д.
Структура Ыо
Основной структурой данных общего слоя работы с блочными устройствами
является дескриптор текущей операции ввода/вывода для блочного устройст-
устройства, который называется Ыо. Каждый дескриптор Ыо включает в себя иденти-
идентификатор для области памяти на диске (номер первого сектора и количество
секторов в этой области) и один или несколько сегментов, описывающих об-
области памяти, вовлеченные в операцию ввода/вывода. Дескриптор Ыо реали-
реализован структурой Ыо, поля которой перечислены в табл. 14.1.
Таблица 14.1. Поля структуры Ыо
Тип Поле Описание
sector_t bi_sector Первый сектор на диске, участвующий
в операции ввода/вывода
struct bio * bi_next Ссылка на следующую структуру bio
в очереди запросов
struct block_device * bi_bdev Указатель на дескриптор блочного
устройства
unsigned long bi_flags Флаги состояния bio
unsigned long bi_rw Флаги операции ввода/вывода
unsigned short bi_vcnt Количество сегментов в массиве
bio_vec, принадлежащем структуре bio
unsigned short bi_idx Текущий индекс в массиве сегментов
bio_vec, принадлежащем структуре bio
unsigned short bi_phys_segments Количество физических сегментов струк-
структуры bio после слияния
Таблица 14.1 (окончание)
Тип Поле Описание
unsigned short bi_hw_segments Количество аппаратных сегментов после
слияния
unsigned int bi_size Количество байтов, подлежащих
пересылке
unsigned int bi_hw_front_size Используется алгоритмом слияния
аппаратных сегментов
unsigned int bi_hw_back_size Используется алгоритмом слияния
аппаратных сегментов
unsigned int bi_max_vecs Максимально допустимое количество
сегментов в массиве bio_vec, принад-
принадлежащем структуре bio
struct bio_vec * bi_io_vec Указатель на массив сегментов bio_vec,
принадлежащий структуре bio
bio_end_io_t * bi_end_io Метод, вызываемый в конце операции
ввода/вывода
atomic_t bi_cnt Счетчик ссылок для bio
void * bi_private Указатель, используемый общим слоем
работы с блочными устройствами и ме-
методом завершения ввода/вывода, вызы-
вызываемом драйвером блочного устройства
bio_destructor_t * bi_destructor Метод-деструктор (как правило,
bio_destructor ()), вызываемый при
освобождении bio
Каждый сегмент в bio представлен структурой biovec, поля которой пере-
перечислены в табл. 14.2. Поле biiovec указывает на первый элемент массива
структур biovec, а поле bivcnt хранит текущее количество элементов в мас-
массиве.
Таблица 14.2. Поля структуры bio_vec
Тип Поле Описание
struct page * bv_page Указатель на дескриптор страничного кадра данного
сегмента
unsigned int bv_len Длина сегмента в байтах
unsigned int bv_of f set Смещение данных сегмента внутри страничного кадра
Содержимое дескриптора bio меняется по ходу операции ввода/вывода. На-
Например, если драйвер блочного устройства не может выполнить DMA-
пересылку всех данных вразброс за одну операцию, то поле biidx обновля-
обновляется, чтобы драйвер помнил о первом сегменте в Мо, который еще не пере-
переслан. Для перебора сегментов Ыо (начиная с текущего, с индексом biidx)
драйвер может ВЫПОЛНИТЬ макрос bio_for_each_segment.
Когда общий слой работы с блочными устройствами начнет новую операцию
ввода/вывода, он выделит новую структуру bio с помощью функции
bio_aiioc(). Обычно структуры bio выделяются slab-аллокатором, но ядро
поддерживает небольшой пул памяти, который используется при нехватке
оперативной памяти (см. главу 8). Ядро поддерживает также и пул для струк-
структур biovec: в конце концов, нет смысла выделять bio, не имея возможности
выделить дескрипторы сегментов, которые нужно включить в состав bio.
Функция bioput () уменьшает счетчик ссылок bio (bicnt) и, когда счетчик
доходит до нуля, освобождает структуру bio и связанные с ней структуры
bio_vec.
Представление дисков и разделов на диске
Диск — это логическое блочное устройство, которым управляет общий слой
работы с блочными устройствами. Обычно диск соответствует аппаратному
блочному устройству, например, жесткому диску, гибкому диску или ком-
компакт-диску. Впрочем, диск может быть и виртуальным устройствам, постро-
построенным на основе нескольких дисковых разделов, или областью памяти, рас-
расположенной на определенных страницах ОЗУ. В любом случае верхние ком-
компоненты ядра одинаково работают со всеми дисками благодаря службам,
которые предоставляет общий слой работы с блочными устройствами.
Диск представлен объектом gendisk, поля которого перечислены в табл. 14.3.
Таблица 14.3. Поля объекта gendisk
Тип Поле Описание
int major Старший номер диска
int f irstminor Первый младший номер, связанный
с диском
int minors Диапазон младших номеров, связанных
с диском
char [32] disk_name Имя диска, используемое по соглашению
(как правило, это каноническое имя соот-
соответствующего файла устройства)
struct hd_struct ** part Массив дескрипторов разделов диска
struct fops Указатель на таблицу методов блочного
block_device_operations * устройства
Таблица 14.3 (окончание)
Тип Поле Описание
struct request_queue * queue Указатель на очередь запросов к диску
void * private_data Закрытые данные драйвера блочного
устройства
sector_t capacity Размер области памяти диска
(в секторах)
int flags Флаги, описывающие тип диска
char [64] devfs_name Имя файла устройства в специальной
файловой системе devfs
int number Более не используется
struct device * driverf s_dev Указатель на объект device, соответст-
соответствующий аппаратному устройству
struct kobject kobj Встроенный объект kobject
struct timerrandstate * random Указатель на структуру, записывающую
время возникновения прерываний на
диске; она используется встроенным в
ядро генератором случайных чисел
int policy Установлено в 1, если диск доступен
только для чтения (операция записи за-
запрещена); в противном случае равно О
atomict syncio Счетчик секторов, записанных на диск;
используется только для RAID
unsigned long stamp Отметка времени для определения ста-
статистики обращений к очереди
unsigned long stamp_idle Аналогично предыдущему
int in_f light Количество текущих операций
ввода/вывода
struct disk_stats * dkstats Статистика обращений к диску каждым
из процессоров
Поле flags хранит информацию о диске. Самым важным флагом является
genhdflup. Если он установлен, значит, диск инициализирован и работает.
Другой важный флаг, genhdflremovable, установлен у съемного диска, тако-
такого как гибкий диск или компакт-диск.
Поле fops объекта gendisk указывает на таблицу block_device_operations,
которая содержит несколько специальных методов для необходимых опера-
операций блочного устройства (табл. 14.4).
Таблица 14.4. Методы блочных устройств
Метод Действие
open Открытые файлы блочного устройства
release Закрытие последней ссылки на файл блочного устройства
ioctl Выполнение системного вызова ioctl () для файла блочного
устройства (с использованием глобальной блокировки ядра)
compat_ioctl Выполнение системного вызова ioctl () для файла блочного
устройства (без использования глобальной блокировки ядра)
media_changed Проверка, был ли заменен съемный диск (например, дискета)
revalidate_disk Проверка допустимости данных на блочном устройстве
Жесткие диски обычно разбиты на логические разделы. Каждый файл блоч-
блочного устройства может представлять либо целый диск, либо раздел на диске.
Например, master-диск EIDE может быть представлен файлом устройства
/dev/hda со старшим номером 3 и младшим номером 0. Первые два раздела
этого диска представляются файлами /dev/hda 1 и /dev/hda2 со старшим номе-
номером 3 и младшими номерами 1 и 2. Вообще, разделы на диске характеризу-
характеризуются последовательными младшими номерами.
Если диск разбит на разделы, информация о них хранится в массиве структур
hdstruct, адрес которого содержится в поле part объекта gendisk. Массив
индексирован по относительному индексу раздела на диске. Поля дескрипто-
дескриптора hdstruct перечислены в табл. 14.5.
Таблица 14.5. Поля дескриптора hd_struct
Тип Поле Описание
sector_t start_sect Начальный сектор раздела на диске
sector_t nr_sects Длина раздела (количество секторов)
struct kobject kobj Встроенный объект kobject
unsigned int reads Количество операций чтения, выполненных для
данного раздела
unsigned int read_sectors Количество секторов, прочитанных из раздела
unsigned int writes Количество операций записи, выполненных для
данного раздела
unsigned int write_sectors Количество секторов, записанных в раздел
int policy Установлено в 1, если раздел доступен только для
чтения, и 0 в противном случае
int partno Относительный индекс раздела на диске
Когда ядро обнаруживает новый диск в системе (на этапе загрузки, или когда
съемный носитель вставляется в дисковод, или когда внешний диск подсо-
подсоединяется во время работы системы), оно вызывает функцию aiiocdisko,
которая выделяет и инициализирует новый объект gendisk и, если диск раз-
разбит на разделы, создает соответствующий массив дескрипторов hdstruct.
Затем ядро вызывает функцию adddisko, чтобы вставить новый дескриптор
gendisk в структуры общего слоя работы с блочными устройствами
(см. разд. "Регистрация и инициализация драйвера устройства'1 далее в этой
главе).
Выдача запроса
В этом разделе мы опишем типичную последовательность действий, выпол-
выполняемых ядром при выдаче запроса на операцию ввода/вывода общему слою
работы с блочными устройствами. Мы будем предполагать, что запрошенные
порции данных идут на диске подряд, и что ядро уже определило их физиче-
физическое местоположение.
Первый шаг заключается в вызове функции bioaiioco выделения нового
дескриптора Мо. Затем ядро инициализирует этот дескриптор, устанавливая в
нем некоторые поля:
□ полю bisector присваивается номер первого сектора данных (если блоч-
блочное устройство разбито на несколько разделов, номер сектора считается от
начала раздела);
□ в поле bisize записывается количество секторов, содержащих данные;
□ полю bibdev присваивается адрес дескриптора блочного устройства (см.
разд. "Блочные устройства" далее в этой главе);
□ полю biiovec присваивается адрес начала массива структур biovec,
каждая из которых описывает сегмент (буфер в памяти), вовлеченный в
операцию ввода/вывода. Кроме того, в поле bivcnt записывается общее
количество сегментов в дескрипторе bio;
□ полю birw присваиваются флаги запрошенной операции. Самый важный
флаг определяет направление движения данных: read @) или write A);
□ полю biendio присваивается адрес завершающей процедуры, которая
выполняется по окончании операции ввода/вывода.
После успешной инициализации дескриптора bio ядро вызывает функцию
genericmakerequest (), КОТОрая ЯВЛЯетСЯ главной ТОЧКОЙ ВХОДа В общий СЛОЙ
работы с блочными устройствами.
Эта функция выполняет следующие действия:
1. Проверяет, не превышает ли значение bio->bi_sector количество секторов
блочного устройства. Если превышает, функция устанавливает поле
biojeof в поле bio->bi_f lags, выводит сообщение ядра об ошибке, вызы-
вызывает функцию bioendio () и завершает свою работу. Функция bioendio ()
обновляет поля bisize и bisector дескриптора bio и вызывает метод
biendio этого дескриптора. Реализация последней функции сильно за-
зависит от того, какой компонент ядра запустил операцию ввода/вывода.
В следующих главах мы рассмотрим несколько примеров методов
bi_end_io.
2. Получает в свое распоряжение очередь запросов q, ассоциированную с
блочным устройством (см. разд. "Дескрипторы очередей запросов" далее в
этой главе). Адрес этой очереди хранится в поле bddisk дескриптора
блочного устройства, а на дескриптор указывает поле bio->bi_bdev.
3. Вызывает функцию blockwaitqueuerunning (), Чтобы удостовериться,
что в данный момент не происходит динамическая замена планировщика
ввода/вывода. Если это не так, функция блокирует выполнение процесса,
пока не будет запущен новый планировщик ввода/вывода (см.
разд. "Планировщик ввода/вывода" далее в этой главе).
4. Вызывает функцию bikpartitionremap (), чтобы проверить, ссылается ли
блочное устройство на раздел диска (bio->bi_bdev не равно bio->bi_
dev->bd_contains (см. разд. "Блочные устройства" далее в этой главе).
В таком случае функция получает дескриптор hdstruct раздела диска из
поля bio->bi_bdev и выполняет над ним следующие действия:
• обновляет ПОЛЯ read_sectors И reads (ИЛИ write_sectors И writes) деск-
риптора hdstruct в соответствии с направлением движения данных;
• изменяет поле bio->bi_sector, чтобы преобразовать номер сектора от-
относительно начала раздела в номер сектора на уровне диска;
• присваивает полю bio->bi_bdev адрес дескриптора блочного устройст-
устройства — ДИСКа В целом (bio->bd_contains).
С этого момента общий слой работы с блочными устройствами, плани-
планировщик ввода/вывода и драйвер устройства забывают о разбивке диска на
разделы и работают с целым диском.
5. Вызывает метод q->make_request_fn, чтобы поместить запрос bio в очередь
запросов q.
6. Возвращает управление.
Типичную реализацию метода makerequestfn мы обсудим в разд. "Выдача
запроса планировщику ввода/вывода" далее в этой главе.
Планировщик ввода/вывода
Хотя драйверы блочных устройств способны передавать один сектор за раз,
общий слой работы с блочными устройствами не выполняет отдельную опе-
операцию для каждого сектора. Это сильно понизило бы производительность
диска, поскольку поиск физического положения сектора на поверхности дис-
диска требует много времени. Ядро пытается, по возможности, сгруппировать
несколько секторов и обрабатывать их как единое целое, уменьшая тем са-
самым среднее число перемещений головки.
Когда компоненту ядра нужно прочитать или записать какие-то данные, он
фактически создает запрос к блочному устройству. Этот запрос описывает
потребность в секторах, и какая операция (чтение или запись) должна быть
выполнена над ними. Однако ядро не удовлетворяет запрос непосредственно
после его создания. Операция ввода/вывода планируется, но будет выполнена
позже. Эта искусственная задержка парадоксальным образом повышает про-
производительность блочных устройств. Когда запрашивается передача нового
блока, ядро проверяет, можно ли его удовлетворить, чуть расширив преды-
предыдущий запрос, который еще ждет своего выполнения (то есть можно ли вы-
выполнить новый запрос без дополнительных операций поиска). Поскольку об-
обращения к диску, как правило, являются последовательно, этот механизм
очень эффективен.
Откладывание запросов на более позднее время усложняет работу с блочны-
блочными устройствами. Предположим, например, что процесс открывает обычный
файл, и, следовательно, драйвер файловой системы хочет прочитать с диска
соответствующий индексный дескриптор. Драйвер блочного устройства ста-
ставит запрос в очередь, и процесс приостанавливается, пока не будет передан
блок, в котором хранится индексный дескриптор. Однако драйвер блочного
устройства сам не может быть блокирован, потому что тогда любой другой
процесс, пытающийся обратиться к тому же диску, тоже будет блокирован.
Чтобы не допустить приостановки драйвера блочного устройства, каждая
операция ввода/вывода обрабатывается асинхронно. В частности, драйверы
блочных устройств управляются прерываниями (см. главу 13). Общий слой
работы с блочными устройствами вызывает планировщик ввода/вывода для
создания нового запроса к блочному устройству или расширения уже суще-
существующего запроса и заканчивает свою работу. Драйвер блочного устройства,
активизируемый чуть позже, вызывает процедуру-стратега, чтобы та выбрала
"подвешенный" запрос и удовлетворила его, выдав необходимые команды
контроллеру диска. Когда операция ввода/вывода завершается, контроллер
диска возбуждает прерывание, и соответствующий обработчик снова вызыва-
вызывает процедуру-стратега для обработки другого ждущего запроса, если это не-
необходимо.
Каждый драйвер блочного устройства поддерживает собственную очередь
запросов, которая содержит список запросов к устройству, ожидающих вы-
выполнения. Если контроллер диска управляет несколькими дисками, то для
каждого физического блочного устройства обычно существует одна очередь
запросов. Планирование ввода/вывода выполняется для каждой очереди за-
запросов отдельно, что повышает производительность.
Дескрипторы очередей запросов
Каждая очередь запросов представлена большой структурой requestqueue,
поля которой перечислены в табл. 14.6.
Таблица 14.6. Поля дескриптора очереди запросов
Тип Поле Описание
struct list_head queue_head Список ждущих запросов
struct request * last_merge Указатель на дескриптор того запро-
запроса в очереди, который должен быть
первым рассмотрен на предмет
возможного слияния
elevator_t * elevator Указатель на объект-лифт
struct request_list rq Структура, используемая при выде-
выделении дескрипторов запросов
request_fn_proc * request_fn Метод, реализующий точку входа
в процедуру-стратега данного
драйвера
merge_request_fn * back_merge_fn Метод, проверяющий, возможно ли
слить bio с последним запросом
в очереди
merge_request_fn * front_merge_fn Метод, проверяющий, возможно ли
слить bio с первым запросом
в очереди
merge_requests_fn * merge_requests_fn Метод, пытающийся слить два
соседних запроса в очереди
make_request_f n * make_request_f n Метод, вызываемый, когда в очередь
нужно занести новый запрос
prep_rq_fn * prep_rq_fn Метод, строящий команды, которые
следует отправить аппаратному
устройству для обработки данного
запроса
unplug_f n * unplug_f n Метод для "откупоривания" блочного
устройства
Таблица 14.6 (продолжение)
Тип Поле Описание
merge_bvec_fn * merge_bvec_fn Метод, возвращающий количество
байтов, которые могут быть вставле-
вставлены в существующий bio при добав-
добавлении нового сегмента (обычно не
определен)
activity_fn * activityfn Метод, вызываемый, когда запрос
добавляется в очередь (обычно не
определен)
issue_f lush_fn * issue_f lush_fn Метод, вызываемый при очистке
очереди (очередь опустошается путем
обработки всех запросов подряд)
struct timer_list unplug_timer Динамический таймер, используемый
для "закупоривания" блочного
устройства
int unplugthresh Если количество ждущих запросов
в очереди превышает это значение,
устройство немедленно "откупорива-
"откупоривается" (по умолчанию 4)
unsigned long unplug_delay Задержка перед "откупориванием"
(по умолчанию 3 мс)
struct work_struct unplug_work Рабочая очередь, используемая при
отключении устройства
struct backing_dev_infо backing_dev_infо Описание дано после таблицы
void * queuedata Указатель на закрытые данные
драйвера блочного устройства
void * activity_data Закрытые данные, используемые
методом activity_fn
unsigned long bounce_pfn Номер страничного кадра, выше
которого следует применить
контроль границ буферов
int bounce_gfp Флаги выделения памяти для
промежуточных буферов
unsigned long queue_f lags Набор флагов, описывающих
состояние очереди
spinlock_t * queue_lock Указатель на блокировку очереди
запросов
struct kobject kobj Встроенный объект kobject
для очереди запросов
unsigned long nr_requests Максимальное количество запросов
в очереди
Таблица 14.6 (продолжение)
Тип Поле Описание
unsigned int nr_congestion_on Очередь считается переполненной,
если количество ждущих запросов
превышает этот порог
unsigned int nr_congestion_of f Очередь считается не переполнен-
переполненной, если количество ждущих запро-
запросов опускается ниже этого порога
unsigned int nr_batching Максимальное количество (обыч-
(обычно 32) запросов, которые могут быть
представлены, даже если очередь
переполнена, специальным процес-
сом-"пакетником"
unsigned short max_sectors Максимальное количество секторов,
обрабатываемых одним запросом
(настраивается)
unsigned short max_hw_sectors Максимальное количество секторов,
обрабатываемых одним запросом
(аппаратное ограничение)
unsigned short max_phys_segments Максимальное количество физиче-
физических сегментов, обрабатываемых
одним запросом
unsigned short max_hw_segments Максимальное количество аппарат-
аппаратных сегментов, обрабатываемых
одним запросом (то есть максималь-
максимальное количество отдельных областей
памяти при DMA-пересылке враз-
вразброс)
unsigned short hardsect_size Размер сектора в байтах
unsigned int max_segment_size Максимальный размер физического
сегмента
unsigned long seg_boundary_mask Маска границы памяти для операции
слияния сегментов
unsigned int dma_alignment Битовая карта выравнивания
для начального адреса и длины
DMA-буферов (по умолчанию 511)
struct blk_queue_tag * queue_tags Битовая карта тегов "свободен/занят"
(используется для запросов с тегами)
atomic_t re font Счетчик ссылок
unsigned int inf light Количество ждущих запросов
в очереди
unsigned int sg_timeout Тайм-аут для команды, определяе-
определяемый пользователем (применяется
только в устройстве SCSI)
Таблица 14.6 (окончание)
Тип Поле Описание
unsigned int sg_reserved_size He используется
struct listhead drain_list Голова списка запросов, отложенных
на то время, пока динамически заме-
заменяется планировщик запросов
По своей организации очередь запросов является двунаправленным списком,
состоящим из дескрипторов запросов (то есть структур request). Поле
queuehead дескриптора очереди запросов содержит голову (первый пустой
элемент) списка, а указатели в поле queueiist дескриптора запроса связыва-
связывают каждый запрос с предыдущим и следующим элементами списка. Порядок
следования элементов в списке специфичен для каждого драйвера устройст-
устройства, но планировщик ввода/вывода предлагает несколько стандартных спосо-
способов упорядочивания элементов, которые обсуждаются далее в разд. "Пла-
"Планировщик ввода/вывода".
Поле backingdevinf о является небольшим объектом ТИПа backingdevinfo,
в котором хранится информация о трафике ввода/вывода для аппаратного
блочного устройства. Например, этот объект содержит информацию об опе-
опережающем чтении и о переполненности очереди запросов.
Дескриптор запросов
Каждый ждущий запрос к блочному устройству представлен дескриптором
запроса, который хранится в структуре request, описанной в табл. 14.7.
Таблица 14.7. Поля дескриптора запроса
Тип Поле Описание
struct list_head queueiist Указатели для списка запросов
unsigned long flags Флаги запроса
sector_t sector Номер следующего передаваемо-
передаваемого сектора
unsigned long nr_sectors Количество секторов, которые
еще подлежат передаче
в запросе
unsigned int current_nr_sectors Количество секторов в текущем
сегменте текущей структуры bio,
подлежащих передаче
Таблица 14.7 (продолжение)
Тип Поле Описание
sector_t hard_sector Номер следующего передаваемо-
передаваемого сектора
unsigned long hard_nr_sectors Количество секторов, которые
еще подлежат передаче в запро-
запросе (обновляется общим слоем
работы с блочными устройства-
устройствами)
unsigned int hard_cur_sectors Количество секторов в текущем
сегменте текущей структуры bio,
подлежащих передаче (обновля-
(обновляется общим слоем работы
с блочными устройствами)
struct bio * bio Первый bio в запросе, еще не
переданный полностью
struct bio * biotail Последний bio в списке запроса
void * elevator_private Указатель на закрытые данные
планировщика ввода/вывода
int rq_status Статус запроса: либо rq_active,
Либо RQ_INACTIVE
struct gendisk * rq_disk Дескриптор диска, на который
ссылается запрос
int errors Счетчик ошибок ввода/вывода,
возникших при текущей передаче
unsigned long start_time Время начала запроса (в тиках)
unsigned short nr_phys_segments Количество физических сегмен-
сегментов в запросе
unsigned short nr_hw_segments Количество аппаратных сегмен-
сегментов в запросе
int tag Тег, ассоциированный с запросом
(только для аппаратных
устройств, поддерживающих
множественные пересылки
данных, ожидающие выполнения)
char * buffer Указатель на буфер в памяти для
текущей пересылки данных (null,
если буфер находится в верхней
памяти)
int ref_count Счетчик ссылок для запроса
request_queue_t * q Указатель на дескриптор очере-
очереди, содержащей данный запрос
Таблица 14.7 (окончание)
Тип Поле Описание
struct request_list * rl Указатель на структуру
request_list
struct completion * waiting Структура completion для ожи-
ожидания конца пересылки данных
void * special Указатель на данные, используе-
используемые, когда запрос включает
в себя "специальную" команду
для аппаратного устройства
unsigned int cmd_len Длина команд в поле and
unsigned char [] cmd Буфер, содержащий заранее под-
подготовленные команды, построен-
построенные методом prep_rq_fn, при-
принадлежащем очереди запросов
unsigned int data_len Обычно это длина данных
в буфере, на который указывает
поле data
void * data Указатель, используемый драй-
драйвером устройства для отслежи-
отслеживания пересылаемых данных
unsigned int sense_len Длина буфера, на который указы-
указывает поле sense @, если поле
sense равно NULL)
void * sense Указатель на буфер, используе-
используемый для вывода sense-команд
unsigned int timeout Время ожидания для запроса
struct request_pm_state * pm Указатель на структуру, исполь-
используемую командами управления
питанием
Каждый запрос состоит из одной или нескольких структур bio. Первоначаль-
Первоначально общий слой работы с блочными устройствами создает запрос, включаю-
включающий в себя только одну структуру. Впоследствии планировщик ввода/вывода
может "расширить" запрос либо добавляя новые сегменты к оригинальной
структуре bio, либо включая дополнительные структуры bio в запрос. Это
может произойти, если новые данные оказываются физически смежными с
данными, затребованными в запросе. Поле bio дескриптора запроса указыва-
указывает на первую структуру bio в запросе, а поле biotaii — на последнюю. Мак-
Макрос rqforeachbio реализует цикл, перебирающий все структуры bio, вхо-
входящие в запрос.
Некоторые поля дескриптора запроса могут изменяться динамически. На-
Например, как только будут переданы все порции данных, на которые ссыла-
ссылалась структура Ыо, поле Ыо будет обновлено так, чтобы оно указывало на
следующую структуру Мо в запросе. В то же время в конец списка запроса
могут быть добавлены новые структуры Ыо, так что и поле biotaii может
измениться.
Некоторые другие поля дескриптора запроса модифицируются либо плани-
планировщиком ввода/вывода, либо драйвером устройства по ходу передачи секто-
секторов диска. Например, поле nrsectors содержит количество секторов, кото-
которые еще подлежат передаче в запросе, а поле currentnrsectors — количе-
количество секторов, подлежащих передаче в текущей Ыо.
Поле flags содержит большое количество флагов, перечисленных в
табл. 14.8. Самым важным из них является reqrw, который определяет на-
направление движения данных.
Таблица 14.8. Флаги дескриптора запроса
Флаг Описание
req_rw Направление движения данных
reqfailfast В запросе указывается, что не следует повторять попытку опе-
операции ввода/вывода, если возникла ошибка
reqsoftbarrier Запрос действует как барьер для планировщика ввода/вывода
req_hardbarrier Запрос действует как барьер для планировщика ввода/вывода и
для драйвера устройства; он должен быть отработан после
старых запросов, но до новых
reqcmd Запрос включает в себя обычное чтение или запись данных
reqnomerge Запрос не должен расширяться или сливаться с другими запро-
запросами
reqstarted В данный момент запрос обрабатывается
reqdontprep Не вызывать метод prep_rq_f n запроса, т. е. не подготавли-
подготавливать заранее команды, которые следует отправить аппаратному
устройству
req_queued Запрос имеет тег, т. е. он относится к аппаратному устройству,
способному одновременно управлять несколькими пересылка-
пересылками данных, ждущими выполнения
REQPC Запрос содержит прямую команду аппаратному устройству
reqblockpc То же, что и предыдущий флаг, но команда, включена в bio
reqsense Запрос включает в себя sense-команду считывания (для уст-
устройств SCSI и ATAPI)
Таблица 14.8 (окончание)
Флаг Описание
reqfailed Устанавливается, когда sense-команда или прямая команда
в запросе не сработала, как ожидалось
reqquiet В запросе указывается, что не следует генерировать сообще-
сообщения ядра в случае ошибок ввода/вывода
req_special Запрос включает в себя специальную команду аппаратному
устройству (например, сброс)
req_drive_cmd Запрос включает в себя специальную команду дискам IDE
reqdrivetask Запрос включает в себя специальную команду дискам IDE
reqdrivetaskfile Запрос включает в себя специальную команду дискам IDE
req_preempt Данный запрос замещает текущий запрос в начале очереди
(только для дисков IDE)
req_pm_suspend Запрос содержит команду управления памятью, переводящую
устройство в состояние ожидания
req_pm_resume Запрос содержит команду управления питанием, переводящую
устройство в рабочее состояние
req_pm_shutdown Запрос содержит команду управления питанием, отключающую
устройство
req_bar_preflush Запрос содержит команду "очистить очередь", которую следует
отправить контроллеру диска
req_bar_postflush Запрос содержит команду "очистить очередь", которая была
отправлена контроллеру диска
Управление выделением дескрипторов запросов
При высокой активности обращений к диску ограниченный объем динамиче-
динамической памяти может стать узким местом для процессов, добавляющих новые
запросы в очередь q. Чтобы справиться с подобной ситуацией, каждый деск-
дескриптор requestqueue содержит структуру requestiist, которая состоит из:
□ указателя на пул памяти для дескрипторов запросов (см. главу 8);
П двух счетчиков, отслеживающих количество дескрипторов запросов, вы-
выделенных для чтения и записи соответственно;
□ двух флагов, показывающих, возникли ли ошибки при последнем выделе-
выделении дескрипторов запросов для чтения и записи соответственно;
□ двух очередей, содержащих приостановленные процессы, ожидающие вы-
выделения дескрипторов запросов для чтения и записи соответственно;
□ очереди из процессов, ожидающих, когда очередь запросов будет прину-
принудительно опустошена.
Функция bikgetrequesto пытается получить свободный дескриптор запро-
са из пула памяти, выделенного для данной очереди запросов. Если памяти не
хватает, а пул исчерпан, функция либо приостанавливает текущий процесс,
либо (если управляющий тракт ядра не может быть блокирован) возвращает
null. В случае успешного выделения дескриптора запроса функция сохраняет
в поле ri дескриптора адрес структуры requestiist, принадлежащей данной
очереди запросов. Функция bikputrequest () освобождает дескриптор за-
запроса, причем, если его счетчик ссылок стал равен нулю, дескриптор возвра-
возвращается в пул, из которого был взят.
Борьба с переполнением очереди запросов
Для каждой очереди установлено максимальное количество ждущих запро-
запросов. Поле nrrequests дескриптора запроса хранит это максимальное число
для пересылки данных в каждом направлении. По умолчанию очередь может
содержать до 128 ждущих запросов на запись и столько же на чтение. Если
количество ждущих запросов на чтение (запись) превысит значение
nrrequests, очередь помечается как заполненная до отказа путем установки
флага QUEUE_FLAG_READFULL (QUEUE_FLAG_WRITEFULL) В ПОЛе queue_flags деск-
риптора очереди запросов. Процессы, которые могут быть блокированы, но
пытаются добавить запросы на пересылку данных в этом направлении, при-
приостанавливаются, т. е. заносятся в соответствующую очередь структуры
request_list.
Заполненная до отказа очередь запросов отрицательно влияет на производи-
производительность системы, потому что она вынуждает многие процессы находиться в
приостановленном состоянии в ожидании завершения операций вво-
ввода/вывода. Поэтому, если количество запросов, ждущих пересылку данных в
определенном направлении, превысит значение поля nrcongestionon деск-
дескриптора запроса (по умолчанию равное 113), то ядро уже считает очередь пе-
переполненной и пытается понизить скорость создания новых запросов. Пере-
Переполненная очередь запросов перестает считаться таковой, когда количество
запросов в ней опускается ниже значения поля nrcongestionof f (по умолча-
умолчанию 111). Функция bikcongestionwait о приостанавливает текущий запрос
до тех пор, пока очередь не перестанет считаться переполненной, либо до
истечения некоторого времени.
Активизация драйвера блочного устройства
Как мы видели ранее, целесообразно отложить активизацию драйвера блоч-
блочного устройства, чтобы повысить вероятность группирования запросов, отно-
сящихся к смежным блокам. Задержка реализуется с помощью техники, из-
известной как закупоривание и откупоривание устройства2. Как только устрой-
устройство закупоривается, его драйвер становится неактивным, даже если в его
очередях есть необработанные запросы.
Функция bikpiugdeviceo закупоривает блочное устройство, или, точнее,
очередь запросов, обслуживаемую некоторым драйвером блочного устройст-
устройства. Функция принимает в качестве аргумента адрес q дескриптора очереди
запросов. Она устанавливает бит queueflagplugged в поле q->queue_f lags, a
затем запускает динамический таймер, встроенный в поле q->unpiug_timer.
Функция bikremovepiugo откупоривает очередь запросов q, т. е. сбрасывает
бит QUEUEFLAGPLUGGED И Останавливает ДИНаМИЧеСКИЙ Таймер q->unplug_
timer. Эта функция может быть явно вызвана ядром, когда все запросы, кото-
которые находятся "в поле зрения" и могут быть слиты, добавлены в очередь.
Кроме того, планировщик ввода/вывода откупоривает очередь запросов, если
количество ждущих запросов превышает значение в поле unpiugthresh деск-
дескриптора очереди запросов (по умолчанию равное 4).
Если устройство остается закупоренным в течение интервала времени, ука-
указанного в q->unpiug_deiay (по умолчанию 3 миллисекунды), динамический
таймер, запущенный функцией bik_piug_device(), заканчивает отсчет време-
времени, в результате чего вызывается функция bikunpiugtimeout о. Вследствие
этого поток ядра kblockd, обслуживающий рабочую очередь kbiockd_
workqueue, возобновляет свое выполнение (см. главу 4). Этот поток ядра вы-
выполняет функцию, адрес которой хранится в структуре q->unpiug_work, т. е.
фуНКЦИЮ blk_unplug_work(). А ЭТа фуНКЦИЯ ВЫЗЫВает МеТОД q->unplug_fn
очереди запросов, который обычно реализуется функцией generic_unpiug_
device (). Она выполняет собственно откупоривание устройства: вначале про-
проверяет, активна ли очередь, затем вызывает функцию bikremovepiugo, на-
наконец, выполняет процедуру-стратега (то есть метод requestfn), чтобы за-
запустить обработку следующего запроса в очереди (см. разд. "Регистрация и
инициализация драйвера устройства" далее в этой главе).
Алгоритмы планирования ввода/вывода
Когда в очередь запросов добавляется новый запрос, общий слой работы с
блочными устройствами вызывает планировщик ввода/вывода, чтобы тот оп-
определил точное положение нового элемента в очереди. Планировщик стара-
старается поддерживать очередь упорядоченной по секторам. Если запросы, под-
2 Если вам непонятны термины "закупоривание" и "откупоривание", считайте их эквивалентными
словам "деактивизация" и "активизация" соответственно.
лежащие обработке, брать из списка поочередно, то время поиска на диске
существенно сократится, поскольку головка будет двигаться в одном направ-
направлении, от внутренней дорожки к внешней (или наоборот), а не дергаться слу-
случайным образом от одной дорожки к другой. Такой подход напоминает алго-
алгоритм, используемый в лифтах при обработке вызовов, поступающих с разных
этажей от людей, которые хотят спуститься или подняться. Лифт двигается в
одном направлении. Когда он доходит до последнего заказанного этажа, он
меняет направление движения. По аналогии, планировщики ввода/вывода
иногда называются лифтами.
При большой нагрузке алгоритм планирования ввода/вывода, предписываю-
предписывающий строго придерживаться порядка номеров секторов, будет работать пло-
плохо. В самом деле, в таком случае время окончания пересылки данных будет
сильно зависеть от физического положения данных на диске. Тогда, если
драйвер устройства будет обрабатывать запросы в начале очереди (с мень-
меньшими номерами секторов), и новые запросы с малыми номерами секторов
будут все время добавляться в очередь, то запросы в хвосте очереди никогда
не дождутся обработки. Получается, что планирование ввода/вывода является
довольно сложной задачей.
В настоящее время Linux 2.6 предлагает четыре типа планировщиков вво-
ввода/вывода, или лифтов. Они называются Anticipatory (Предвидящий),
Deadline (Крайний срок), CFQ (Complete Fairness Queueing, Абсолютно чест-
честная очередь) и Noop (No Operation, Никаких действий). Лифт, используемый
ядром по умолчанию для большинства блочных устройств, указывается на
этапе загрузки в параметре ядра eievator=<MMn>, где <имя> — это одна из та-
таких последовательностей: as, deadline, cfq и noop. Если на этапе загрузки ар-
аргумент не задан, ядро использует планировщик Anticipatory. В любом случае
драйвер устройства может заменить лифт, установленный по умолчанию, на
другой. Кроме того, драйвер может определить собственный алгоритм пла-
планирования ввода/вывода, но это делается крайне редко.
Более того, системный администратор может во время работы системы заме-
заменить планировщик ввода/вывода конкретного блочного устройства. Напри-
Например, чтобы заменить планировщик, используемый master-диском первого
IDE-канала, администратор может записать имя лифта в файл /sys/block
/hda/queue/scheduler специальной файловой системы sysfs (см. главу 13).
Алгоритм планирования ввода/вывода, используемый в очереди запросов,
представлен объектом-лифтом, имеющим тип elevatort. Его адрес хранится
в поле elevator дескриптора очереди запросов. Объект-лифт имеет несколько
методов, покрывающих все возможные операции над лифтом: связывание
лифта с очередью запросов и отсоединение от нее, добавление запросов в
очередь, слияние запросов в очереди, удаление запросов из очереди, получе-
ние из очереди следующего запроса для обработки и т. д. Лифт содержит
также адрес таблицы со всей информацией, необходимой для управления
очередью запросов. Кроме того, каждый дескриптор запроса имеет поле
elevatorprivate, которое указывает на дополнительную структуру данных,
используемую планировщиком ввода/вывода при обработке запроса.
Далее мы кратко опишем четыре алгоритма планирования ввода/вывода, от
простейшего до самого сложного. Хотим предупредить читателя, что разра-
разработка планировщика ввода/вывода во многом напоминает разработку плани-
планировщика центрального процессора: эвристика и значения констант подбира-
подбираются в результате большого количества экспериментов и тестов.
Вообще говоря, во всех алгоритмах используется диспетчерная очередь, ко-
которая содержит все запросы, расставленные в том порядке, в каком они
должны быть обработаны драйвером; следующий запрос, подлежащий обра-
обработке, всегда стоит первым в диспетчерной очереди. Фактически, эта оче-
очередь — не что иное, как очередь запросов с корнем в поле queuehead деск-
дескриптора очереди запросов. Почти все алгоритмы пользуются дополнитель-
дополнительными очередями для классификации и сортировки запросов. Все они
позволяют драйверу добавлять структуры Мо к существующим запросам и,
если необходимо, объединять "смежные" запросы.
Лифт Noop
Это простейший алгоритм планирования ввода/вывода. Упорядоченная оче-
очередь отсутствует, новые запросы всегда добавляются либо в начало, либо в
конец диспетчерной очереди, а следующий запрос, подлежащий обработке,
всегда стоит первым.
ЛифтСРО
Основной целью лифта CFQ является гарантия справедливого разделения
пропускной способности диска между всеми процессами, выдавшими запро-
запросы на ввод/вывод. Для достижения этой цели лифт пользуется большим ко-
количеством отсортированных очередей (по умолчанию 64), которые содержат
запросы, поступающие от различных процессов. Когда запрос передается
лифту, ядро вызывает хеш-функцию, преобразующую идентификатор группы
потоков текущего процесса (обычно этот идентификатор соответствует PID)
в индекс очереди. Затем лифт добавляет новый запрос в конец этой очереди.
Таким образом, запросы, поступившие от одного процесса, всегда находятся
в одной очереди.
Для заполнения диспетчерной очереди лифт сканирует очереди по кругу, вы-
выбирает первую непустую и переносит из нее пакет запросов в диспетчерную
очередь.
Лифт Deadline
Кроме диспетчерной очереди, лифт Deadline использует еще четыре очереди.
Две из них, отсортированные очереди, содержат, соответственно, запросы на
чтение и на запись, упорядоченные по первым номерам секторов. Две других,
очереди крайнего срока, содержат те же запросы, упорядоченные по "край-
"крайним срокам" их выполнения. Эти очереди призваны не допустить "голодной
смерти" запросов, т. е. ситуации, при которой лифт долгое время игнорирует
отдельные запросы, предпочитая обрабатывать те, что находятся ближе к по-
последнему обработанному запросу. Крайний срок выполнения запроса опреде-
определяется таймером, который стартует, когда запрос передается лифту. По умол-
умолчанию время ожидания запросов на чтение составляет 500 мс, а запросов на
запись — 5 с. Запросы на чтение имеют более высокий приоритет, чем запро-
запросы на запись, потому что они обычно блокируют процессы, выдавшие их.
Крайций срок выполнения гарантирует, что планировщик обработает запрос,
долго ждущий своей очереди, даже если запрос находится далеко от начала
списка.
Когда лифт должен пополнить диспетчерную очередь, он вначале определяет
направление движения данных у следующего запроса. При наличии запросов,
как на чтение, так и на запись, лифт выбирает направление "чтение", если
только направление "запись" не было отвергнуто много раз ("голодная
смерть" запросов на запись недопустима).
Затем лифт просматривает очередь крайнего срока для выбранного направле-
направления. Если крайний срок выполнения первого запроса в очереди истек, лифт
переносит этот запрос в конец диспетчерной очереди. Кроме того, он перено-
переносит туда пакет запросов из отсортированной очереди, начиная с запроса, сле-
следующего за тем, у которого истек крайний срок. Длина такого пакета зависит
от того, относятся ли запросы в нем к физически смежным порциям данных
на диске (чем больше "смежных" запросов, тем длиннее пакет).
Если же ни у одного запроса крайний срок еще не истек, лифт переносит в
диспетчерную очередь пакет запросов, начиная с запроса, следующего за по-
последним запросом, взятым из отсортированной очереди. Когда курсор дости-
достигает конца отсортированной очереди, ее просмотр возобновляется с начала
(так называемый лифт с односторонним движением).
Лифт Anticipatory
Лифт Anticipatory является самым сложным планировщиком ввода/вывода,
предлагаемым Linux. Он основан на лифте Deadline, у которого он позаимст-
позаимствовал базовый механизм: две очереди крайнего срока и две отсортированные
очереди. Планировщик постоянно сканирует отсортированные очереди, чере-
чередуя запросы на чтение с запросами на запись, но отдавая предпочтение пер-
вым. Сканирование происходит последовательно, если у какого-то запроса не
истек крайний срок выполнения. По умолчанию крайний срок выполнения
запросов на чтение составляет 125 мс, а запросов на запись — 250 мс. Впро-
Впрочем, лифт использует дополнительную эвристику:
□ в некоторых случаях лифт может выбрать запрос, стоящий перед текущей
позицией в отсортированной очереди, заставляя головку двигаться над
диском в обратном направлении. Как правило, это происходит, если рас-
расстояние поиска для этого запроса меньше половины расстояния поиска за-
запроса, стоящего после текущей позиции в отсортированной очереди;
□ лифт собирает статистику о характере операций ввода/вывода, выполняе-
выполняемых каждым процессом в системе. Сразу после размещения запроса на
чтение, поступившего от процесса Р, лифт проверяет, принадлежит ли
следующий запрос в отсортированной очереди тому же процессу Р. Если
это так, следующий запрос заносится в диспетчерную очередь немедлен-
немедленно. В противном случае лифт просматривает статистику, относящуюся к
процессу Р. Если лифт решит, что Р, вероятно, вскоре выдаст еще один за-
запрос на чтение, он подождет некоторое время (по умолчанию 7 мс). Таким
образом лифт может предвидеть запрос на чтение от процесса Р, "близ-
"близкий" на диске к только что размещенному запросу от того же процесса.
Выдача запроса планировщику ввода/вывода
Как было сказано ранее в этой главе, функция genericmakerequesto вызы-
вызывает метод makerequestfn дескриптора очереди запросов, чтобы передать
запрос планировщику ввода/вывода. Этот метод обычно реализуется функци-
функцией makerequest (), которая принимает в качестве параметров дескриптор
requestqueue (параметр q) и дескриптор bio (параметр bio). Функция выпол-
выполняет следующие действия:
1. Вызывает функцию bik_queue_bounce() для установки промежуточного
буфера, если он необходим. Если промежуточный буфер уже был создан,
функция makerequest () работает с ним, а не с оригинальным bio.
2. Вызывает функцию планировщика elvqueueemptyo, чтобы проверить,
имеются ли ждущие запросы в очереди запросов. Обратите внимание, что
диспетчерная очередь может быть пуста, но другие очереди планировщика
ввода/вывода могут содержать ждущие запросы. Если ждущих запросов
нет, описываемая функция вызывает функцию bikpiugdevice () для за-
закупоривания очереди запросов (см. разд. "Активизация драйвера блочного
устройства"ранее в этой главе) и переходит к шагу 5.
3. На этот шаг функция попадает, если очередь запросов не пуста. Функция
вызывает функцию планировщика elvmerge (), которая проверяет, может
ли новый bio быть включен в существующий запрос, и возвращает одно из
трех значений:
• elevatornomerge — bio не может быть включен в существующий за-
запрос. В таком случае описываемая функция переходит к шагу 5;
• elevatorbackmerge — bio может быть добавлен в качестве последнего
дескриптора в некоторый запрос req. В таком случае функция вызывает
МеТОД q->back_merge_fn, ЧТОбы Проверить, МОЖНО ЛИ расширить ЭТОТ
запрос. Если нельзя, она переходит к шагу 5. В противном случае
функция заносит дескриптор bio в конец списка, принадлежащего за-
запросу req и обновляет поля этого запроса. Затем она пытается слить за-
запрос со следующим (не исключено, что новый bio заполнил "дыру"
между двумя запросами);
• elevator_front_merge — bio может быть добавлен в качестве первого
дескриптора в некоторый запрос req. В таком случае функция вызывает
метод q->f rontmergefn, чтобы проверить, можно ли расширить этот
запрос. Если нельзя, она переходит к шагу 5. В противном случае
функция заносит дескриптор bio в начало списка, принадлежащего за-
запросу req и обновляет поля этого запроса. Затем она пытается слить за-
запрос с предыдущим в очереди.
4. Итак, bio вставлен в уже существующий запрос. Функция переходит к ша-
шагу 7, чтобы выполнить заключительные действия.
5. На этот шаг функция попадает, если bio должен быть вставлен в новый
запрос. Функция выделяет дескриптор нового запроса. В случае нехватки
памяти функция приостанавливает текущий процесс, если не установлен
флаг biorwahead в поле bio->bi_rw, означающий, что данная операция
является операцией опережающего чтения (см. главу 16). Если флаг уста-
установлен, функция вызывает функцию bioendio () и завершает свою работу,
т. к. пересылка данных выполнена не будет. Описание функции bioendio ()
приведено в шаге 1 описания функции genericmakerequesto ранее в
этой главе.
6. Инициализирует поля дескриптора запроса. В частности:
• инициализирует различные поля, где хранятся номера секторов, теку-
текущий bio и текущий сегмент, в соответствии с содержимым дескриптора
bio;
• устанавливает флаг reqcmd в поле flags (нормальная операция чтения
или записи);
• если страничный кадр первого сегмента bio находится в нижней памя-
памяти, функция записывает в поле buffer линейный адрес этого буфера;
• Записывает В ПОЛе rq_disk адрес bio->bi_bdev->bd_disk;
• вставляет bio в список запросов;
• Присваивает ПОЛЮ start_time Значение jiffies.
7. Все сделано. Однако перед завершением работы функция проверяет, уста-
установлен ли флаг biorwsync в поле bio->bi_rw. Если установлен, она вызы-
вызывает generic_unplug_device() применительно К ОЧереДИ Запросов, чтобы
откупорить драйвер (см. разд. "Активизация драйвера блочного устрой-
устройства" ранее в этой главе).
8. Завершает работу.
Если очередь запросов не была пуста перед вызовом функции make
request (), то либо очередь запросов уже откупорена, либо она будет откупо-
откупорена вскоре, поскольку у каждой закупоренной очереди запросов q, содержа-
содержащей ждущие запросы, есть включенный динамический таймер q->unpiug_
timer. С другой стороны, если очередь запросов была пуста, функция
_make_request () закупоривает ее. Рано (после выхода ИЗ make request (),
если флаг biorwsync установлен) или поздно (когда таймер откупоривания
закончит отсчет) очередь запросов будет откупорена. Как бы то ни было, в
конечном счете, процедура-стратег драйвера блочного устройства позаботит-
позаботится о запросах в диспетчерной очереди (см. разд. "Регистрация и инициализа-
инициализация драйвера устройства" далее в этой главе).
Функция blk_queue_bounce()
Функции blkqueuebounce () анализирует флаги В ПОЛе q->bounce_gfp И ПОрОГ
в поле q->bounce_pfn, чтобы определить, требуется ли контроль границ буфе-
буферов. Необходимость в этом возникает, когда некоторые из буферов запроса
расположены в верхней памяти, и аппаратное устройство не может адресо-
адресовать их.
Старые схемы DMA для шин ISA работали только с 24-битовыми физиче-
физическими адресами. В этом случае порог контроля границ буферов устанавлива-
устанавливается равным 16 Мбайт, что соответствует страничному кадру номер 4096.
Однако драйверы блочных устройств обычно не применяют контроля границ
буферов, когда работают со старыми устройствами. Вместо этого они пред-
предпочитают сразу выделять буферы прямого доступа к памяти в зоне памяти
ZONE_DMA.
Если аппаратное устройство не в состоянии работать с буферами в верхней
памяти, функция проверяет, действительно ли для некоторых буферов в bio
необходим контроль границ. Если это так, она делает копию дескриптора bio,
создавая промежуточный bio. Затем для каждого сегмента, у которого стра-
ничный кадр имеет номер, больший или равный значению q->bounce_pfn,
функция выполняет следующие действия:
1. Выделяет страничный кадр в зоне zonenormal или zonedma в соответствии
с флагами выделения.
2. Обновляет поле bvpage сегмента в промежуточном Ыо так, чтобы оно
указывало на дескриптор нового страничного кадра.
3. Если поле bio->bio_rw задает операцию записи, функция вызывает функ-
функцию kmap (), чтобы временно отобразить страницу верхней памяти в адрес-
адресное пространство ядра, копирует страницу из верхней памяти в страницу в
нижней памяти и вызывает функцию kunmap (), чтобы снять отображение.
Затем функция bikqueuebounce () устанавливает флаг biobounced в проме-
промежуточном bio, инициализирует метод biendio, специфичный для промежу-
промежуточного bio, и сохраняет в поле biprivate (принадлежащем промежуточно-
промежуточному bio) указатель на оригинальный bio. Когда пересылка данных, связанная с
промежуточным bio, завершается, функция, реализующая метод biendio,
скопирует данные в буфер в верхней памяти (только для операции чтения) и
освобождает промежуточный bio.
Драйверы блочных устройств
Драйверы блочных устройств являются компонентами самого нижнего уров-
уровня в подсистеме блочного ввода/вывода операционной системы Linux. Они
получают запросы от планировщика ввода/вывода и обрабатывают их долж-
должным образом.
Драйверы блочных устройств, конечно же, интегрированы в модель драйвера
устройства, описанную в главе 13. Следовательно, каждый из них связан с
дескриптором devicedriver. Кроме того, каждый диск, с которым работает
драйвер, связан с дескриптором device. Однако эти дескрипторы имеют до-
довольно общий характер, и подсистема блочного ввода/вывода должна хра-
хранить дополнительную информацию для каждого блочного устройства в сис-
системе.
Блочные устройства
Драйвер блочного устройства может управлять несколькими устройствами.
Например, драйвер IDE может работать с несколькими IDE-дисками, каждый
из которых является самостоятельным блочным устройством. Кроме того,
каждый диск обычно разбит на разделы, каждый из которых можно тракто-
трактовать как логическое блочное устройство. Очевидно, что драйвер должен по-
заботиться обо всех системных вызовах VFS, выполняемых для файлов блоч-
блочных устройств, ассоциированных с этими устройствами.
Каждое блочное устройство представлено дескриптором biockdevice, поля
которого перечислены в табл. 14.9.
Таблица 14.9. Поля дескриптора блочного устройства
Тип Поле Описание
devt bddev Старший и младший номера
блочного устройства
struct inode * bdinode Указатель на индексный дескрип-
дескриптор файла, ассоциированного
с блочным устройством в файло-
файловой системе bdev
int bd_openers Счетчик количества открытий
блочного устройства
struct semaphore bdsem Семафор, защищающий открытие
и закрытие блочного устройства
struct semaphore bd_mount_sem Семафор, применяемый для
запрета новых монтирований на
блочном устройстве
struct listhead bdinodes Голова списка индексных деск-
дескрипторов открытых файлов дан-
данного блочного устройства
void * bdholder Текущий держатель дескриптора
блочного устройства
int bdholders Счетчик многократных установок
поля bd_holder
struct block_device * bd_contains Если блочное устройство являет-
является разделом диска, данное поле
указывает на дескриптор всего
диска; в противном случае оно
указывает на этот же дескриптор
блочного устройства
unsigned bd_block_size Размер блока
struct hdstruct * bdpart Указатель на дескриптор раздела
(null, если данное блочное уст-
устройство не является разделом
диска)
unsigned bd_part_count Счетчик того, сколько раз были
открыты разделы, входящие
в состав данного блочного
устройства
Таблица 14.9 (окончание)
Тип Поле Описание
int bd_invalidated Флаг, устанавливаемый, когда
необходимо прочитать таблицу
разделов данного блочного уст-
устройства
struct gendisk * bd_disk Указатель на структуру gendisk
диска, соответствующего данно-
данному блочному устройству
struct list_head * bd_list Указатели для списка дескрипто-
дескрипторов блочных устройств
struct backing_dev_infо * bd_inode_backing_ Указатель на специализирован-
dev_inf о ный дескриптор
backing_dev_infо ДЛЯ блочного
устройства (как правило, null)
unsigned long bd_private Указатель на закрытые данные
держателя блочного устройства
Все устройства блочных устройств занесены в глобальный список, голова
которого представлена переменной aiibdevs. Указатели, связывающие эле-
элементы этого списка, находятся в поле bdiist дескриптора блочного устрой-
устройства.
Если дескриптор блочного устройства относится к разделу диска, поле
bdcontains указывает на дескриптор блочного устройства, ассоциированный
с диском в целом, в то время как поле bdpart указывает на дескриптор раз-
раздела hdstruct (см. разд. "Представление дисков и разделов на диске" ранее в
этой главе). В противном случае, т. е. когда дескриптор блочного устройства
относится ко всему диску, поле bdcontains указывает на сам этот дескрип-
дескриптор, а поле bdpartcount подсчитывает, сколько раз были открыты разделы
на диске.
Поле bdhoider хранит линейный адрес, представляющий держателя блочно-
блочного устройства. Держатель — это не драйвер, обслуживающий ввод/вывод на
данном устройстве. Это компонент ядра, который пользуется блочным уст-
устройством и имеет по отношению к нему исключительные особые привилегии
(например, он имеет свободный доступ к полю bdprivate дескриптора блоч-
блочного устройства). Как правило, держателем блочного устройства является
файловая система, смонтированная на нем. Другим типичным случаем явля-
является ситуация, в которой файл блочного устройства открыт для исключи-
исключительного доступа: здесь держателем является соответствующий файловый
объект.
Функция bdciaimo записывает в поле bdhoider указанный адрес, а функция
bdreiease (), наоборот, сбрасывает это поле в null. Необходимо отдавать се-
себе отчет в том, что один и тот же компонент ядра может вызывать bd_ciaim()
много раз, и каждый вызов увеличивает значение поля bdhoiders. Чтобы ос-
освободить блочное устройство, этот компонент ядра должен вызвать
bdre lease () соответствующее количество раз.
Рис. 14.3 относится к диску в целом и иллюстрирует, как дескрипторы блоч-
блочных устройств связаны с остальными важными структурами подсистемы
блочного ввода/вывода
Рис. 14.3. Связь дескрипторов блочных устройств
с другими структурами подсистемы блочного ввода/вывода
Обращение к блочному устройству
Когда ядро получает запрос на открытие файла блочного устройства, оно
должно вначале определить, открыт ли этот файл. Дело в том, что если он
уже открыт, ядру не нужно создавать и инициализировать новый дескриптор
блочного устройства. Вместо этого ему будет достаточно обновить уже су-
существующий. Ситуация усложняется тем, что файлы блочных устройств с
одинаковыми старшими и младшими номерами, но разными путями, воспри-
воспринимаются виртуальной файловой системой как разные файлы, хотя относятся
к одному блочному устройству. Следовательно, ядро не может определить,
используется ли блочное устройство, путем простой проверки наличия объ-
объекта для файла устройства в кэше индексных дескрипторов.
Соответствие старшего и младшего номеров дескриптору блочного устройст-
устройства поддерживается с помощью файловой системы bdev (см. разд. Специаль-
Специальные файловые системы" главы 12). Каждый дескриптор блочного устройства
связан со специальным файлом этой системы: поле bdinode дескриптора ука-
зывает на соответствующий индексный дескриптор в bdev; и, наоборот, этот
индексный дескриптор кодирует старший и младший номера блочного уст-
устройства, а также адрес его дескриптора.
Функция bdget () принимает в качестве параметра старший и младший номе-
номера блочного устройства и ищет в файловой системе bdev ассоциированный
индексный дескриптор. Если таковой не находится, функция выделяет новый
индексный дескриптор и новый дескриптор блочного устройства. В любом
случае функция возвращает адрес дескриптора блочного устройства, соответ-
соответствующего указанным старшему и младшему номерам.
После того, как дескриптор для данного блочного устройства найден, ядро
может определить, используется ли блочное устройство в данный момент.
Для этого ядро проверяет значение поля bdopeners: если оно положительно,
значит, блочное устройство используется (возможно, при посредстве другого
файла устройства). Кроме того, ядро поддерживает список индексных деск-
дескрипторов, относящихся к открытым файлам блочных устройств. Корень этого
списка находится в поле bdinodes дескриптора блочного устройства, а поле
idevices индексного дескриптора содержит указатели на предыдущий и сле-
следующий элементы этого списка.
Регистрация и инициализация
драйвера устройства
В этом разделе мы опишем основные шаги по настройке нового драйвера для
блочного устройства. Очевидно, что приведенное описание весьма кратко,
тем не менее оно поможет понять, как и где инициализируются основные
структуры, используемые подсистемой блочного ввода/вывода.
Мы обойдем молчанием многие шаги, необходимые при инициализации лю-
любого драйвера и уже упомянутые в главе 13. Например, мы пропустим все
шаги, связанные с регистрацией самого драйвера (см. разд. "Модель драйвера
устройства" главы 13). Как правило, блочное устройство принадлежит к
стандартной архитектуре шины, такой как PCI или SCSI, и ядро предлагает
вспомогательные функции, которые в качестве побочного эффекта регистри-
регистрируют драйвер в модели драйвера устройства.
Определение
специализированного дескриптора драйвера
Во-первых, драйверу устройства нужен специализированный дескриптор foo
типа f oodevt, который содержит данные, необходимые для управления ап-
аппаратным устройством. Дескриптор любого устройства будет хранить ин-
информацию, такую как порты ввода/вывода, используемые для программиро-
вания устройства, IRQ-линия для прерываний, возбуждаемых устройством,
внутреннее состояние устройства и т. д. Дескриптор также должен включать
в себя несколько полей, нужных подсистеме блочного ввода/вывода:
struct foo_dev_t {
[...]
spinlock_t lock;
struct gendisk *gd;
[...]
} foo;
Поле lock является спин-блокировкой, используемой для защиты полей деск-
дескриптора foo. Ее адрес часто передается вспомогательным функциям ядра, ко-
которые таким образом могут защитить структуры подсистемы блочного вво-
ввода/вывода, специфичные для данного драйвера. Поле gd является указателем
на дескриптор gendisk, представляющий все блочное устройство (диск),
управляемое этим драйвером.
Резервирование старшего номера
Драйвер устройства должен зарезервировать старший номер для своих целей.
По ТраДИЦИИ ЭТО ДелаеТСЯ С ПОМОЩЬЮ фуНКЦИИ register_blkdev () :
err = register_blkdev(FOO_MAJOR, "foo");
if (err) goto error_major_is_busy;
Эта функция во многом похожа на функцию register_chrdev(), представлен-
представленную в главе 13: она резервирует старший номер foomajor и ассоциирует с
ним имя foo. Обратите внимание, что здесь нет способа выделить поддиапа-
поддиапазон младших номеров, поскольку отсутствует аналог функции register_
chrdevregion (). Кроме того, между зарезервированным старшим номером и
структурами данных драйвера не устанавливается никакой связи. Единствен-
Единственный видимый результат работы registerbikdevo — это помещение нового
элемента в список зарегистрированных старших номеров в специальном фай-
файле /proc/devices.
Инициализация специализированного дескриптора
Все поля дескриптора foo должны быть корректно проинициализированы
прежде, чем драйвер станет к ним обращаться. Для инициализации полей,
имеющих отношение к подсистеме блочного ввода/вывода, драйвер должен
выполнить следующий код:
spin_lock_init(&fоо.lock);
foo.gd = alloc_diskA6);
if (!foo.gd) goto error_no_gendisk;
Драйвер инициализирует спин-блокировку, затем выделяет дескриптор дис-
диска. Как видно из рис. 14.3, структура gendisk является критичной для работы
подсистемы блочного ввода/вывода, поскольку она ссылается на множество
других структур с данными. Функция aiiocdisko выделяет также массив
для хранения дескрипторов разделов диска. В качестве аргумента она прини-
принимает количество элементов hdstruct в этом массиве. Если оно равно 16,
драйвер может поддерживать диски, содержащие до 15 разделов (нулевой
раздел не используется).
Инициализация дескриптора gendisk
Затем драйвер инициализирует некоторые поля дескриптора gendisk:
fоо.gd->private_data = &foo;
foo.gd->major = FOO_MAJOR;
foo.gd->first_minor = 0;
foo.gd->minors = 16;
set__capacity(foo.gd, foo_disk_capacity_in_sectors);
strcpy(foo.gd->disk_name, "foo");
foo.gd->fops = &foo_ops;
Адрес дескриптора foo Сохраняется В ПОЛе private_data Структуры gendisk,
чтобы низкоуровневые функции драйвера, вызванные подсистемой блочного
ввода/вывода в качестве методов, могли быстро найти дескриптор драйвера;
это повышает производительность, если драйвер управляет несколькими дис-
дисками одновременно. Функция setcapacity () инициализирует поле capacity
емкостью диска, измеряемой в количестве 512-байтовых секторов. Это зна-
значение, по всей вероятности, определяется зондированием аппаратной части и
опросом параметров диска.
Инициализация таблицы методов блочного устройства
Поле fops дескриптора gendisk инициализируется адресом специализирован-
специализированной таблицы методов блочного устройства (см. табл. 14.4K. Вполне вероятно,
что таблица f ooops драйвера устройства содержит функции, специфичные
для этого драйвера. Например, если аппаратное устройство поддерживает
съемные диски, то общий слой работы с блочными устройствами вполне мо-
может вызвать метод mediachanged для проверки, был ли вставлен новый диск с
момента последней операции монтирования или открытия файла на этом уст-
устройстве. Проверка обычно производится путем отправки контроллеру не-
нескольких низкоуровневых команд, и поэтому метод mediachanged всегда спе-
специфичен для драйвера.
3 Не следует путать методы блочного устройства с файловыми операциями файла блочного уст-
устройства.
Аналогичным образом, метод iocti вызывается только тогда, когда общий
слой работы с блочными устройствами не знает, как обрабатывать какую-
либо команду iocti. Например, этот метод обычно вызывается, когда сис-
системный вызов iocti () пытается определить геометрию диска, т. е. количест-
количество цилиндров, дорожек, секторов и головок. Реализация этого метода тоже
специфична для драйвера.
Выделение и инициализация очереди запросов
В этот момент разработчик драйвера должен создать очередь запросов, кото-
которая будет хранить запросы, ожидающие обслуживания. Это легко сделать
с помощью следующего кода:
foo.gd->rq = blk_init_queue(foo_strategy, &foo.lock);
if (!foo.gd->rq) goto error_no_request_queue;
blk_queue_hardsect_size(foo.gd->rd, foo_hard_sector_size);
blk_queue_max_sectors(foo.gd->rd, foo_max_sectors);
blk_queue_max_hw_segments (f oo. gd->rd, f oo_max_hw_segments) ;
blk_queue_max_phys_segments (f oo. gd->rd, f oo_max_phys_segments) ;
Функция bikinitqueueo выделяет дескриптор очереди запросов и инициа-
инициализирует многие его поля значениями по умолчанию. Она принимает в каче-
качестве параметров адрес спин-блокировки дескриптора устройства (для поля
foo.gd->rq->queue_iock) и адрес процедуры-стратега, входящей в состав
драйвера (для поля foo. gd->rq->request_fn) (см. разд. "Процедура-стратег"
далее в этой главе). Кроме того, функция bikinitqueueo инициализирует
поле foo.gd->rq->eievator, заставляя драйвер использовать алгоритм по
умолчанию для планирования ввода/вывода. Если драйверу понадобится дру-
другой лифт, он сможет впоследствии переопределить адрес в поле elevator.
Затем некоторые вспомогательные функции записывают в различные поля
дескриптора очереди запросов значения, предложенные драйвером (поля
с аналогичными именами можно посмотреть в табл. 14.6).
Настройка обработчика прерываний
Как было сказано в главе 4, драйвер должен зарегистрировать линию IRQ для
устройства. Это можно сделать следующим образом:
request_irq(foo_irq, foo_interrupt,
SAJENTERRUPT|SA_SHIRQ, "foo", NULL);
Функция foointerrupt() является обработчиком прерываний устройства.
Мы обсудим некоторые ее особенности далее, в разд. "Обработчик преры-
прерываний".
Регистрация диска
Итак, все структуры драйвера устройства готовы. Последний этап инициали-
инициализации заключается в регистрации и активизации диска. Для этого достаточно
одной строчки:
add_disk(fоо.gd);
Функция adddisko принимает адрес дескриптора gendisk и выполняет сле-
следующие действия:
1. Устанавливает флаг genhdflup в поле gd->fiags.
2. Вызывает функцию kobj_map(), чтобы создать связь между драйвером и
старшим номером устройства с его диапазоном младших номеров (см.
разд. "Драйверы символьных устройств" главы 13); необходимо понимать,
что в этом случае область отображения объекта kobject представлена пе-
переменной bdevmap.
3. Регистрирует объект kobject, включенный в дескриптор gendisk, в модели
драйвера устройства в качестве нового устройства, обслуживаемого дан-
данным драйвером (например, /sys/block/foo).
4. Сканирует таблицу разделов диска, если таковая имеется. Для каждого
найденного раздела инициализирует соответствующий дескриптор hdstruct
в массиве foo.gd->part. Регистрирует разделы в модели драйвера устрой-
устройства (например, /sys/block/foo/fool).
5. Регистрирует объект kobject, встроенный в дескриптор очереди запросов,
в модели драйвера устройства (например, /sys/block/foo/queue).
После того как функция adddisko возвратит управление, драйвер заработа-
заработает. Функция, выполнившая инициализацию, завершается. Процедура-стратег
и обработчик прерываний позаботятся об обслуживании каждого запроса,
направленного драйверу планировщиком ввода/вывода.
Процедура-стратег
Процедура-стратег представляет собой функцию (или группу функций) драй-
драйвера блочного устройства, которая взаимодействует с аппаратным блочным
устройством с целью удовлетворения запросов, находящихся в диспетчерной
очереди. Процедура-стратег вызывается с помощью метода requestfn деск-
дескриптора очереди запросов. В примере из предыдущего раздела этим методом
является функция f oostrategy (). Слой планировщика ввода/вывода передает
этой функции адрес q дескриптора очереди запросов.
Как мы увидим далее, процедура-стратег обычно запускается после занесения
нового запроса в пустую очередь запросов. После своей активизации драйвер
блочного устройства должен обработать все запросы в очереди и завершить
свое выполнение, когда очередь будет пуста.
Наивная реализация процедуры-стратега может быть следующей: обратиться
к элементу диспетчерной очереди, удалить его из очереди, пообщаться с кон-
контроллером устройства по поводу обслуживания запроса, подождать оконча-
окончания пересылки данных и затем перейти к следующему элементу в диспетчер-
диспетчерной очереди.
Такая реализация, однако, не будет особо эффективной. Даже если предпо-
предположить, что данные будут пересылаться с использованием механизма DMA,
процедура-стратег должна приостановить сама себя, ожидая, пока завершится
операция ввода/вывода. Это означает, что эта процедура должна выполняться
в специально выделенном потоке ядра (мы ведь не хотим наказывать ни в чем
неповинный пользовательский процесс, не так ли?). Более того, такой драй-
драйвер не сможет поддерживать современные контроллеры дисков, способные
обрабатывать несколько операций пересылки данных одновременно.
Поэтому большинство драйверов блочных устройств придерживается сле-
следующей стратегии:
□ процедура-стратег запускает пересылку данных для первого запроса в
очереди и настраивает контроллер блочного устройства так, чтобы он вы-
выдал прерывание, когда пересылка завершится. После этого процедура за-
завершает работу;
□ когда контроллер диска выдаст прерывание, обработчик прерывания снова
вызовет процедуру-стратега (чаще всего, напрямую, но иногда путем ак-
активизации рабочей очереди). Процедура-стратег либо запускает еще одну
пересылку данных для текущего запроса, либо, если все данные для этого
запроса пересланы, удаляет запрос из диспетчерной очереди и приступает
к обработке следующего.
Запросы могут состоять из нескольких bio, которые, в свою очередь, могут
состоять из нескольких сегментов. В принципе, драйверы блочных устройств
могут использовать механизм DMA двумя способами:
□ для каждого сегмента в каждом bio запроса драйвер запускает отдельную
DMA-пересылку;
□ драйвер запускает одну DMA-пересылку вразброс, чтобы обслужить все
сегменты во всех bio этого запроса.
В конечном счете, логика процедуры-стратега зависит от характеристик кон-
контроллера. Любое физическое блочное устройство конструктивно отличается
от всех остальных (например, драйвер гибкого диска группирует блоки в до-
дорожки и передает целую дорожку за одну операцию ввода/вывода), поэтому
нет особого смысла пускаться в общие рассуждения о том, как драйвер уст-
устройства должен обслуживать запросы.
В нашем примере процедура-стратег f oostrategy () выполняет следующие
действия:
1. Получает текущий запрос из диспетчерной очереди, вызывая вспомога-
вспомогательную функцию elvnextrequest () планировщика ввода/вывода. Если
диспетчерная очередь пуста, процедура-стратег возвращает управление:
req = elv_next_request(q);
if (!req) return;
2. Выполняет макрос bikfsrequest, чтобы проверить, установлен ли в за-
запросе флаг reqcmd, т. е. содержит ли запрос обычную операцию вво-
ввода/вывода:
if (!blk_fs_request (req) )
goto handle_special_request;
3. Если контроллер блочного устройства поддерживает DMA-пересылку
вразброс, процедура-стратег программирует контроллер на выполнение
пересылки данных для всего запроса и на выдачу прерывания по оконча-
окончании пересылки. Вспомогательная функция bikrqmapsgo возвращает
список для пересылки вразброс, которым можно немедленно воспользо-
воспользоваться.
4. В противном случае драйвер должен пересылать данные сегментами.
Тогда ПрОЦедура-СТратег ВЫПОЛНЯеТ МаКрОСЫ rq_for_each_bio И bio_
f oreachsegment, которые просматривают список bio и список сегментов
в каждом bio соответственно:
rq_for_each_bio(bio, rq)
bio_for_each_segment(bvec, bio, i) {
/* transfer the i-th segment bvec */
local_irq_save(flags);
addr = kmap_atomic(bvec->bv_page, KM_BIO_SRC_IRQ);
foo start dma transfer(addr+bvec->bv_offset, bvec->bv_len);
kunmap_atornic (bvec->bv_page, KM_BIO_SRC_IRQ) ;
local_irq_restore(flags);
}
ФуНКЦИИ kmap_atomic() И kunmapatomic () необходимы, еСЛИ переСЫЛае-
мые данные могут находиться в верхней памяти. Функция foo_start_
dmatransfero программирует аппаратное устройство на запуск пересыл-
пересылки данных с использованием механизма DMA и на выдачу прерывания по
окончании пересылки.
5. Возвращает управление.
Обработчик прерываний
Обработчик прерываний драйвера блочного устройства активизируется по
окончании пересылки данных с использованием механизма DMA. Он дол-
должен проверить, все ли данные в запросе были пересланы. Если это так, обра-
обработчик прерываний вызывает процедуру-стратега для обслуживания сле-
следующего запроса из диспетчерной очереди. В противном случае обработчик
прерываний обновляет соответствующее поле дескриптора запроса и вызыва-
вызывает процедуру-стратега для обслуживания следующей порции данных.
Приведем типичный фрагмент обработчика прерываний нашего драйвера
устройства f оо:
irqreturn_t foo_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
struct foo_dev_t *p = (struct foo_dev_t *) dev_id;
struct request_queue *rq = p->gd->rq;
[...]
if (!end_that_request_first (rq, uptodate, nr_sectors) ) {
blkdev_dequeue_request (rq) ;
end_that_request_last (rq) ;
}
rq->request_fn(rq);
return IRQ_HANDLED;
}
Задача по завершению запроса разбивается на две функции, названные
end_that_request_f irst () И end_that_request_last ().
Функция endthatrequestf irst () принимает в качестве аргумента дескрип-
дескриптор запроса, флаг, показывающий успешное завершение DMA-пересылки, и
количество пересланных секторов (функция endthatrequestchunk () рабо-
работает аналогично, но вместо количества секторов принимает количество пере-
переданных байтов). Функция сканирует дескрипторы bio в запросе и сегменты
внутри каждого bio, а затем обновляет поля дескриптора запроса следующим
образом:
□ устанавливает поле bio так, чтобы оно указывало на первый незавершен-
незавершенный bio в запросе;
□ устанавливает поле biidx незавершенного bio так, чтобы оно указывало
на первый незавершенный сегмент;
□ устанавливает поле bvof f set и bvien незавершенного сегмента так, что-
чтобы они определяли данные, подлежащие пересылке.
Кроме того, функция вызывает bioendio () для каждого полностью передан-
переданного bio.
Функция endthatrequestfirsto возвращает О, если все данные запроса
пересланы, в противном случае она возвращает 1. Если возвращена единица,
обработчик прерываний снова запускает процедуру-стратега, которая про-
продолжает обработку того же запроса. В противном случае обработчик преры-
прерываний удаляет запрос из очереди запросов (как правило, с помощью функции
blkdevdequeuerequestO), ВЫЗЫВает вспомогательную функцию end_that_
requestiast о и снова запускает процедуру-стратега для обработки следую-
следующего запроса из диспетчерной очереди.
Функция endthatrequestiast () обновляет статистику обращений к диску,
удаляет дескриптор запроса из диспетчерной очереди планировщика
rq->elevator, Возобновляет выполнение Процессов, "СПЯЩИХ" В ПОЛе waiting
дескриптора запроса, и освобождает этот дескриптор.
Открытие файла блочного устройства
Мы завершаем эту главу описанием шагов, выполняемых виртуальной фай-
файловой системой при открытии файла блочного устройства.
Ядро открывает файл блочного устройства каждый раз, когда на диске или
разделе монтируется файловая система, каждый раз, когда активизируется
раздел подкачки, и каждый раз, когда процесс, работающий в режиме поль-
пользователя, выполняет системный вызов open () для файла блочного устройства.
Во всех случаях ядро выполняет, в сущности, одни и те же операции: ищет
дескриптор блочного устройства (возможно, выделяя новый дескриптор, если
устройство еще не используется) и определяет методы файловых операций
для последующих пересылок данных.
В главе 13 мы описали, как функция dentryopen () уточняет методы файлово-
файлового объекта, когда открывается файл устройства. В этом случае в поле fop
файлового объекта находится адрес таблицы defbikfops, содержимое кото-
которого приведено в табл. 14.10.
Таблица 14.10. Операции по умолчанию для файла блочного устройства
(таблица def_blk_fops)
Метод Функция
open blkdev_open()
release blkdev_close()
llseek block_llseek()
read generic_file_read()
write blkdev_file_write()
Таблица 14.10 (окончание)
Метод Функция
aio_read generic_file_aio_read()
aio_write blkdev_file_aio_write()
mmap generic_filejmmap ()
fsync block_fsync()
ioctl block_ioctl()
compat-ioctl compat_blkdev_ioctl()
readv generic_file_readv()
writev generic_file_write_nolock()
sendfile generic_file_sendfile()
Здесь нас интересует только метод open, вызываемый функцией dentry_
open(). Функция bikdevopen() принимает в качестве параметров inode и
f iip, в которых хранятся адреса индексного дескриптора и файлового объек-
объекта соответственно. Она выполняет следующие действия:
1. Вызывает bd_acquire (inode), чтобы получить адрес bdev дескриптора
блочного устройства. Вызванная функция принимает в качестве параметра
адрес индексного дескриптора и выполняет следующие шаги:
• проверяет поле inode->i_bdev индексного дескриптора на равенство
null. Если значение поля не равно null, значит, файл блочного устрой-
устройства уже открыт, и поле содержит адрес соответствующего дескрипто-
дескриптора блочного устройства. В таком случае функция увеличивает счетчик
обращений inode->i_bdev->bd_inode индексного дескриптора специаль-
специальной файловой системы bdev, ассоциированной с блочным устройством,
и возвращает адрес inode->i_bdev дескриптора;
• если файл блочного устройства еще не был открыт, функция вызывает
bdget(inode->i_rdev), чтобы получить адрес дескриптора блочного
устройства в соответствии со старшим и младшим номерами файла
блочного устройства (см. разд. "Блочные устройства" ранее в этой
главе). Если дескриптор еще не существует, функция bdgeto выделяет
его. Обратите внимание, что дескриптор вполне может существовать,
например, если какой-то процесс обратился к этому блочному устрой-
устройству через другой файл устройства;
• сохраняет адрес дескриптора блочного устройства в inode->i_bdev,
чтобы ускорить операции открытия того же файла устройства в буду-
будущем;
• записывает в поле inode->i_mapping значение соответствующего поля в
индексном дескрипторе bdev. Это указатель на объект "адресное про-
пространство", обсуждается в разд. "Объект address space" главы 75;
• заносит inode в список открытых индексных дескрипторов в дескрип-
дескрипторе блочного устройства. Список имеет корень в bdev->bd_inodes;
• возвращает адрес дескриптора bdev.
2. Записывает В ПОЛе f ilp->i_mapping значение inode->i_mapping.
3. Получает адрес дескриптора gendisk, имеющего отношение к данному
блочному устройству:
disk = get_gendisk(bdev->bd_dev, &part);
Если открываемое блочное устройство является разделом, функция воз-
возвращает его индекс в локальной переменной part; в противном случае ус-
устанавливает переменную part в ноль. Функция getgendisko просто вы-
вызывает функцию kobjiookupo на области отображения объекта kobject
bdevmap, передавая ей старший и младший номера устройства (см.
разд. "Регистрация и инициализация драйвера устройства" ранее в этой
главе).
4. Если поле bdev->bd_openers не равно нулю, значит, блочное устройство
уже открыто. Функция проверяет поле bdev->bd_contains:
• если оно равно bdev, блочное устройство представляет собой целый
ДИСК. ФуНКЦИЯ ВЫЗЫВаеТ МеТОД bdev->bd_disk->fops->open блОЧНОГО
устройства, если он определен, затем проверяет поле bdev->
bdinvalidated И ВЫЗЫВаеТ, еСЛИ необходимо, функцию rescan_
partitions();
• если поле не равно bdev, значит, блочное устройство является разделом.
ФуНКЦИЯ увеличивает СЧетЧИК bdev->bd_contains->bd_part_count.
Переходит к шагу 8.
5. На этом шаге функция оказывается, если обращение к блочному устройст-
устройству происходит впервые. Функция инициализирует поле bdev->bd_disk ад-
адресом disk дескриптора gendisk.
6. Если блочное устройство является диском (part равно нулю), функция
выполняет следующие действия:
• выполняет метод блочного устройства disk->fops->open, если таковой
определен. Фактически это специализированная функция, определен-
определенная драйвером блочного устройства, чтобы выполнить специфическую
инициализацию, "в последнюю минуту";
• считывает из поля hardsectsize очереди запросов disk->queue размер
сектора в байтах и использует это значение для корректной установки
ПОЛеЙ bdev->bd_block_size И bdev->bd_inode->i_blkbits. Кроме ТОГО,
записывает в поле bdev->bd_inode->i_size размер диска, вычисленный
ПО значению disk->capacity;
• если установлен флаг bdev->bd_invaiidated, вызывает функцию
rescanpartitionsO, чтобы просканировать таблицу разделов и обно-
обновить их дескрипторы. Флаг устанавливается методом check_disk_
change блочного устройства, который имеется только у съемных дис-
дисков.
7. В противном случае, если блочное устройство является разделом (part не
равно нулю), функция выполняет следующие действия:
• снова вызывает функцию bdget (), на этот раз передавая ей младший
номер disk->first_minor, чтобы получить адрес whole дескриптора
всего диска;
• повторяет шаги с 3 по 6 для дескриптора всего диска, тем самым ини-
инициализируя его, если необходимо;
• записывает в поле bdev->bd_contains адрес дескриптора всего диска;
• увеличивает whoie->bd_part_count, чтобы учесть новую операцию от-
открытия раздела на диске;
• записывает в поле bdev->bd_part значение disk->part [part-i], которое
является адресом дескриптора hdstruct данного раздела. Кроме того,
вызывает kobject_get(&bdev->bd_part->kobj), чтобы увеличить счет-
чик ссылок раздела;
• устанавливает поля индексного дескриптора так, чтобы они содержали
размер раздела и размер его сектора.
8. Увеличивает счетчик bdev->bd_openers.
9. Если файл блочного устройства открывается в режиме исключительного
использования (установлен флаг oexcl в поле fiip->f_fiags), функция
вызывает bd_ciaim(bdev, fiip), чтобы установить держателя блочного
устройства. В случае ошибки, у блочного устройства уже есть держа-
держатель— функция освобождает дескриптор устройства и возвращает код
ОШИбкИ -EBUSY.
10. Завершает выполнение, возвращая 0 (успех).
По окончании функции bikdevopen () системный вызов open о продолжает
работать как обычно. Каждый последующий системный вызов для открытого
файла приведет к выполнению одной из операций по умолчанию, установ-
установленных для файла блочного устройства. Как мы увидим в главе 16, каждая
пересылка данных блочному устройству или от него эффективно реализуется
путем выдачи запросов общему слою работы с блочными устройствами.
ГЛАВА 15
Кэш страниц
Как было сказано в главе 12, кэш диска — это программный механизм, кото-
который позволяет системе хранить в оперативной памяти некоторые данные,
обычно хранящиеся на диске. Такой подход ускоряет обращение к этим дан-
данным в будущем, т. к. избавляет систему от чтения их с диска.
Кэши диска существенно повышают производительность системы, поскольку
повторное обращение к одним и тем же данным на диске случается достаточ-
достаточно часто. Процесс, работающий в режиме пользователя и взаимодействую-
взаимодействующий с диском, может многократно читать и записывать одни и те же данные.
Более того, различные процессы могут в разное время обращаться к одним
данным. В качестве примера рассмотрим команду ср, которая копирует тек-
текстовый файл, а затем вызывает для него текстовый редактор. Для удовлетво-
удовлетворения этих запросов оболочка создаст два разных процесса, обращающихся к
файлу в разные моменты времени.
В главе 12 мы встречали и другие дисковые кэши: кэш элементов каталога, в
котором хранятся объекты, представляющие пути в файловой системе, и кэш
индексных дескрипторов, в котором хранятся объекты, представляющие ин-
индексные дескрипторы на диске. Однако здесь необходимо отметить, что объ-
объекты "элемент каталога" и "индексный дескриптор" — это не просто буферы,
хранящие содержимое каких-то блоков диска, поэтому кэш элементов ката-
каталога и кэш индексных дескрипторов являются довольно необычными кэшами
диска.
В этой главе обсуждается кэш страниц, который представляет собой кэш дис-
диска, работающий с целыми страницами данных. В первом разделе мы дадим
его общее описание. Затем, в разд. "Хранение блоков в кэше страниц", мы
покажем, как можно пользоваться кэшем страниц для получения отдельных
блоков данных, например, суперблоков и индексных дескрипторов (эта воз-
возможность позволяет существенно повысить производительность VFS и дис-
ковых операционных систем). Далее идет разд. "Запись грязных страниц на
диск", в котором описано, как "грязные" страницы в кэше страниц записыва-
записываются обратно на диск. В заключительном разделе, "Системные вызовы syncQ,
fsyncQ и fdatasyncQ", мы упомянем о некоторых системных вызовах, позво-
позволяющих пользователю сбрасывать содержимое кэша страниц на диск, чтобы
обновить информацию на диске.
Кэш страниц
Кэш страниц — это главный дисковый кэш, используемый ядром Linux. Как
правило, ядро обращается к кэшу страниц, когда читает данные с диска или
записывает их. Дня удовлетворения запросов на чтение, исходящих от про-
процессов в режиме пользователя, в кэш добавляются новые страницы. Если
страницы еще нет в кэше, в нем создается новая запись, которая заполняется
данными, прочитанными с диска. Когда памяти достаточно, страница хранит-
хранится в кэше неопределенно долго и может быть использована другими процес-
процессами, что избавляет их от необходимости обращаться к диску.
Аналогичным образом, прежде чем записать страницу данных на блочное
устройство, ядро проверяет, имеется ли в кэше соответствующая страница.
Если нет, в нем создается новая запись, которая заполняется данными, под-
подлежащими записи на диск. Ввод/вывод данных не начинается немедленно;
обновление диска откладывается на несколько секунд, что дает другим про-
процессам шанс еще раз изменить данные, подлежащие записи (иными словами,
в ядре реализованы отложенные операции записи).
Код и структуры ядра никогда не считываются с диска и не записываются на
него. Следовательно, страницы, включенные в кэш, могут быть следующих
типов:
□ страницы, содержащие данные обычных файлов. В главе 16 мы обсудим,
как ядро обрабатывает операции чтения, записи и отображения в память
в отношении этих файлов;
□ страницы, содержащие каталоги; как мы увидим в главе 18, Linux обраба-
обрабатывает каталоги почти так же, как обычные файлы;
□ страницы, содержащие данные, напрямую прочитанные из файлов блоч-
блочных устройств (в обход слоя файловой системы). Как будет показано
в главе 16, ядро обрабатывает их с помощью того же набора функций, что
и страницы, содержащие данные обычных файлов;
□ страницы, содержащие данные процессов, работающих в режиме пользо-
пользователя, которые были выгружены на диск. Как мы увидим в главе 17,
можно заставить ядро держать в кэше страниц несколько страниц, чье со-
содержимое было уже записано в область подкачки (обычный файл или раз-
раздел на диске);
□ страницы, принадлежащие файлам специальных файловых систем, напри-
например, файловой системы shm, которая обеспечивает совместно используе-
используемую память для межпроцессного взаимодействия (см. главу 19).
Нетрудно заметить, что каждая страница, включенная в кэш, содержит дан-
данные, принадлежащие какому-нибудь файлу. Этот файл, а точнее, его индекс-
индексный дескриптор, называется владельцем страницы. (Как мы увидим в гла-
главе 17, страницы, содержащие выгруженные данные, имеют одного и того же
владельца, даже если ссылаются на разные области подкачки.)
Практически все операции read () и write () работают с кэшем страниц. Един-
Единственное исключение бывает, когда процесс открывает файл с установлен-
установленным флагом odirect. В этом случае кэш страниц не используется, а весь
ввод/вывод происходит с участием буферов в адресном пространстве процес-
процесса, работающего в режиме пользователя (см. разд. "Прямой ввод/вывод" гла-
главы 16). Некоторые приложения для работы с базами данных устанавливают
флаг odirect, чтобы иметь возможность применять собственный алгоритм
кэширования.
Разработчики ядра реализовали кэш страниц так, чтобы он удовлетворял
двум главным требованиям:
□ быстро находить конкретную страницу, содержащую данные, имеющие
отношение к указанному владельцу. Чтобы от кэша страниц была заметная
польза, поиск в нем должен происходить очень быстро;
□ хранить информацию о том, как именно должна обрабатываться каждая
страница в кэше при чтении или записи ее содержимого. Например, чте-
чтение страницы из обычного файла, блочного устройства или области вы-
выгрузки должно выполняться по-разному, и ядро должно выбрать правиль-
правильную операцию, в зависимости от того, кто владеет страницей.
Естественно, что единицей информации, хранящейся в кэше страниц, являет-
является страница данных. Как будет показано в главе 18, страница не обязательно
должна содержать физически смежные блоки диска, поэтому ее нельзя иден-
идентифицировать номером устройства и номером блока. Поэтому страница в кэ-
кэше идентифицируется владельцем и индексом внутри данных владельца —
как правило, индексным дескриптором и смещением внутри соответствую-
соответствующего файла.
Объект address_space
Самой важной структурой в кэше страниц является объект addressspace,
структура, встроенная в индексный дескриптор, владеющий страницей1.
1 Исключение имеет место для страниц, которые были выгружены на диск. У этих страниц общий
объект addressspace, не включенный ни в один индексный дескриптор.
В кэше несколько страниц могут относиться к одному владельцу, следова-
следовательно, они могут быть связаны с одним объектом addressspace. Этот объект
также устанавливает связь между страницами определенного владельца и на-
набором методов для этих страниц.
Каждый дескриптор страницы имеет два поля, mapping и index, которые свя-
связывают страницу с кэшем (см. главу 8). Первое поле указывает на объект
addressspace индексного дескриптора, владеющего страницей. Второе со-
содержит смещение в "адресном пространстве" владельца, измеренное в едини-
единицах, равных размеру страницы (иными словами, это позиция данных страни-
страницы внутри дискового образа владельца). Эти два поля используются при по-
поиске страницы в кэше.
Удивительно, но кэш страниц вполне может содержать несколько копий од-
одних и тех же дисковых данных. Например, к одному 4-килобайтовому блоку
данных из обычного файла можно обратиться следующими способами:
□ прочитать файл. В таком случае данные включаются в страницу, которой
владеет индексный дескриптор этого файла;
□ прочитать блок из файла устройства (раздела диска), на котором хранится
обычный файл. В таком случае данные включаются в страницу, которой
владеет главный индексный дескриптор файла блочного устройства.
Таким образом, одни и те же дисковые данные оказываются на двух разных
страницах, на которые указывают два разных объекта addressspace.
Поля объекта addressspace перечислены в табл. 15.1.
Таблица 15.1. Поля объекта address_space
Тип Поле Описание
struct inode * host Указатель на индексный дескрип-
дескриптор, в который включен этот объ-
объект, если таковой имеется
struct radix_tree_root page_tree Корень базисного дерева, иденти-
идентифицирующего страницы данного
владельца
spinlockt treelock Спин-блокировка, защищающая
базисное дерево
unsigned int immapwritable Количество отображений совмест-
совместно используемой памяти в данном
адресном пространстве
struct prio_tree_root ijnmap Корень базисного дерева приори-
приоритетного поиска (см. главу 17)
Таблица 15.1 (окончание)
Тип Поле Описание
struct listhead i_ramap_nonlinear Список областей нелинейного
отображения памяти в адресном
пространстве
spinlock_t immaplock Спин-блокировка, защищающая
базисное дерево приоритетного
поиска
unsigned int truncate_count Счетчик, применяемый при
усечении файла
unsigned long nrpages Общее количество страниц
у данного владельца
unsigned long writeback_index Индекс страницы последней
операции обратной записи для
страниц данного владельца
struct a_ops Методы для страниц данного
address_space_operations * владельца
unsigned long flags Биты ошибок и флаги аллокатора
памяти
struct backing_dev_info * backing_dev_info Указатель на поле
backing_dev_inf о блочного
устройства, хранящего данные
этого владельца
spinlockt private_lock Как правило, это спин-блокировка,
используемая при управлении
списком private_list
struct list head privatelist Как правило, это список "грязных"
буферов косвенных блоков, ассо-
ассоциированных с данным индексным
дескриптором
struct address_space * assoc_mapping Как правило, это указатель на
объект addressspace блочного
устройства, содержащего
косвенные блоки
Если владельцем страницы в кэше является файл, объект addressspace
встраивается в поле idata индексного дескриптора в виртуальной файловой
системе. Поле imapping этого индексного дескриптора всегда указывает на
объект addressspace владельца страниц, содержащих данные этого индекс-
индексного дескриптора. Поле host объекта addressspace указывает на индекс-
индексный дескриптор, в который он встроен.
Таким образом, если страница принадлежит файлу, содержащемуся в файло-
файловой системе Ext3, владельцем страницы является индексный дескриптор это-
го файла, а соответствующий объект addressspace хранится в поле idata
индексного дескриптора в виртуальной файловой системе. Поле imapping
индексного дескриптора указывает на его же поле idata, а поле host объекта
addressspace указывает все на тот же индексный дескриптор.
Впрочем, иногда все обстоит гораздо сложнее. Если страница содержит дан-
данные, прочитанные из файла блочного устройства (данные, полученные от
блочного устройства напрямую), то объект addressspace встроен в "глав-
"главный" индексный дескриптор файла в специальной файловой системе bdev,
ассоциированной с блочным устройством (на этот индексный дескриптор
ссылается поле bdinode дескриптора блочного устройства; см.
разд. "Блочные устройства" главы 14). Таким образом, поле imapping
индексного дескриптора файла блочного устройства указывает на объект
addressspace, встроенный в главный индексный дескриптор; соответственно,
поле host объекта addressspace указывает на главный индексный дескрип-
дескриптор. Следовательно, все страницы, содержащие данные, прочитанные из
блочного устройства, имеют один объект addressspace, даже если обраще-
обращение к ним происходило через различные файлы блочного устройства.
Поля i_mmap, i_mmap_writable, i_mmap_nonlinear И i_mmap_lock имеют ОТНОШе-
ние к отображению в память и обратному отображению. Эти темы обсужда-
обсуждаются в главах 16 и 17.
Поле backingdevinfo указывает на дескриптор backingdevinfo, ассоции-
рованный с блочным устройством, на котором хранятся данные владельца.
Как было разъяснено в разд. "Дескрипторы очередей запросов" в главе 14,
структура backingdevinfo обычно встраивается в дескриптор очереди за-
запросов блочного устройства.
Поле privateiist является головой списка общего назначения, которым
файловая система может пользоваться для своих целей. Например, файловая
система Ext2 собирает в нем "грязные" буферы "косвенных" блоков, ассоции-
ассоциированных с индексным дескриптором (см. разд. "Адресация блоков данных"
главы 18). Когда операция сброса на диск принудительно записывает на диск
индексный дескриптор, ядро сбрасывает также и все буферы из этого списка.
Кроме того, файловая система Ext2 сохраняет в поле assocmapping указатель
на объект addressspace блочного устройства, содержащего "косвенные" бло-
блоки, И ИСПОЛЬЗует СПИН-блОКИроВКу assoc_mapping->private_lock ДЛЯ защиты
списков "косвенных" блоков в мультипроцессорных системах.
Самым важным полем объекта addressspace является aops, которое указы-
указывает на таблицу типа addressspaceoperations. Таблица содержит методы,
определяющие, как нужно обрабатывать страницы, принадлежащие данному
владельцу. Эти методы перечислены в табл. 15.2.
Таблица 15.2. Методы объекта address_space
Метод Описание
writepage Операция записи (со страницы в дисковый образ этого владельца)
readpage Операция чтения (с образа диска на страницу)
sync_page Запуск ввода/вывода, т. е. уже распланированных операций над
страницами, принадлежащими этому владельцу
writepages Записать обратно на диск указанное количество грязных страниц
set_page_dirty Пометить страницу владельца как грязную
readpages Прочитать страницы владельца с диска
preparewrite Подготовить операцию записи (используется дисковыми файловыми
системами)
commit_write Завершить операцию записи (используется дисковыми файловыми
системами)
bmap Получить номер логического блока от индекса блоков файла
invalidatepage Объявить страницы владельца недействительными (используется
при усечении файла)
releasepage Используется журналируемыми файловыми системами для
подготовки к освобождению страницы
directio Прямой ввод/вывод страниц владельца (в обход кэша страниц)
Самыми важными ЯВЛЯЮТСЯ методы readpage, writepage, preparewrite И
coramitwrite. Мы обсудим их в главе 16. В большинстве случаев методы
связывают индексные дескрипторы, владеющие страницами, с драйверами
нижнего уровня, которые обращаются к физическим устройствам. Например,
функция, реализующая метод readpage для индексного дескриптора обычного
файла, знает, как определить позиции блоков, соответствующих каждой
странице файла, на физическом блочном устройстве. Впрочем, нет необхо-
необходимости более подробно обсуждать метОДЫ объекта address_space В ЭТОЙ
главе.
Базисное дерево
В Linux файлы могут быть очень большими, до нескольких терабайт. При
работе с большим файлом кэш страниц может оказаться заполненным таким
большим количеством страниц файлов, что последовательное их сканирова-
сканирование может потребовать слишком много времени. Для повышения эффектив-
эффективности просмотра кэша страниц в Linux 2.6 используется большой набор де-
деревьев ПОИСКа, ПО ОДНОМУ на каждый объект addressspace.
Поле pagetree объекта addressspace является корнем базисного дерева, со-
содержащего указатели на дескрипторы страниц владельца. При заданном ин-
индексе страницы, обозначающем ее позицию в образе диска ее владельца, ядро
может выполнить довольно быстрый просмотр и определить, находится ли в
кэше требуемая страница. При поиске страницы ядро интерпретирует индекс
как путь по базисному дереву и быстро доходит до места, где хранится (или
должен храниться) дескриптор страницы. Найдя дескриптор, ядро может
прочитать его, а также быстро определить, является ли страница "грязной"
(нужно ли ее принудительно записать на диск), и происходит ли в этот мо-
момент ввод/вывод данных страницы.
Каждый узел базисного дерева может иметь до 64 указателей на другие узлы
или на дескрипторы страниц. Узлы нижнего уровня содержат указатели на
дескрипторы страниц (листья), а узлы на более высоких уровнях содержат
указатели на другие узлы (потомки). Каждый узел представлен структурой
radixtreenode, состоящей из трех полей: slots, массива из 64 указателей,
count, счетчика указателей, не равных null, и tags (двухэлементного массива
флагов, который мы обсудим в разд. "Теги базисного дерева" далее в этой
главе). Корень дерева представлен структурой radixtreeroot, которая имеет
три поля, height обозначает текущую высоту дерева (количество уровней,
исключая уровень листьев), gfpmask задает флаги, используемые при запросе
памяти для нового узла, a mode указывает на структуру radixtreenode, со-
соответствующую узлу на уровне 1 (если таковой имеется).
Рассмотрим простой пример. Если ни один из индексов, хранящихся в дереве,
не превышает 63, высота дерева равна 1, потому что все 64 потенциальных
листа могут быть сохранены в узле первого уровня (рис. 15.1, а). Если же в
кэше страниц нужно сохранить новый дескриптор, соответствующий индек-
индексу 131, то высота дерева увеличивается до двух, чтобы оно могло указывать
на индексы вплоть до 4095 (рис. 15.1, б).
В табл. 15.3 приведены максимальные индексы страниц и соответствующие
максимальные размеры файлов для каждой высоты базисного дерева
в 32-разрядной архитектуре. В этом случае наибольшая высота базисного де-
дерева равна шести, хотя маловероятно, что кэш страниц в вашей системе будет
использовать такое огромное дерево. Поскольку индекс страницы хранится в
32-битовой переменной, то при высоте дерева в шесть уровней узел на самом
верхнем уровне может иметь до четырех потомков.
Чтобы лучше понять, как происходит поиск страницы, вспомним, как систе-
система управления страницами использует Таблицы Страниц для преобразования
линейных адресов в физические. Как было сказано в главе 2, 20 старших би-
битов линейного адреса разбиваются на два поля по 10 битов: первое является
смещением в каталоге страниц, а второе — смещением в Таблице Страниц,
на которую указывает запись в каталоге страниц.
Таблица 15.3. Максимальный индекс и максимальный размер файла
для каждой высоты базисного дерева
Высота .. ^ Максимальный
базисного дерева Максимальный индекс размер файла
0 нет 0 байт
1 26-1 = 63 256 Кбайт
2 212-1=4 095 16 Мбайт
3 218-1=262 143 1 Гбайт
4 224-1 = 16 777 215 64 Гбайт
5 230-1 = 1 073 741 823 4 Тбайт
6 232-1 = 4 294 967 295 16 Тбайт
Рис. 15.1. Два примера базисных деревьев
В базисном дереве предпринят аналогичный подход. Эквивалентом линейно-
линейного адреса здесь является индекс страницы. Однако количество полей, рас-
рассматриваемых в индексе страницы, зависит от высоты базисного дерева. Если
она равна 1, представлены только индексы от 0 до 63, и шесть младших битов
индекса страницы интерпретируются как индекс в массиве slots для единст-
единственного узла на уровне 1. Если же базисное дерево имеет высоту 2, то индек-
индексы представляют диапазон от 0 до 4095. Тогда 12 младших битов индекса
страницы разбиваются на два поля по 6 битов. Старшее поле служит индек-
индексом массива для узла на уровне 1, а младшее — индексом массива для узла на
уровне 2. Для любой другой высоты базисного дерева предпринимается ана-
аналогичный подход. При высоте, равной 6, два старших бита индекса страницы
содержат индекс массива для узла первого уровня, следующие 6 битов со-
содержат индекс массива для узла на уровне 2, и т. д. до шести младших битов,
содержащих индекс массива для узла на уровне 6.
Если максимальный индекс в базисном дереве меньше индекса страницы,
которую нужно добавить, ядро соответственно увеличивает высоту дерева.
Содержимое промежуточных узлов базисного дерева зависит от значения ин-
индекса страницы (см. рис. 15.1).
Функции для работы с кэшем страниц
Основные высокоуровневые функции, работающие с кэшем страниц, выпол-
выполняют поиск, добавление и удаление страницы. Еще одна функция обеспечи-
обеспечивает хранение в кэше самой последней версии заданной страницы.
Поиск страницы
Функция f indgetpage () принимает в качестве параметра указатель на объ-
объект addressspace и значение смещения. Она получает спин-блокировку ад-
адресного пространства и вызывает функцию radixtreeiookupo для поиска
листа базисного дерева, имеющего требуемое смещение. Эта функция, в свою
очередь, начинает с корня дерева и проходит вниз в соответствии с битами
смещения, как было объяснено в предыдущем разделе. Встретив указатель
null, функция возвращает null. В противном случае она возвращает адрес
узла, т. е. указатель на дескриптор требуемой страницы. Если запрошенная
страница найдена, функция f indgetpage () увеличивает счетчик обращений,
освобождает спин-блокировку и возвращает адрес страницы; в противном
случае функция освобождает спин-блокировку и возвращает null.
Функция findgetpages() аналогична предыдущей, но выполняет поиск
группы страниц с индексами, идущими подряд. Она принимает в качестве
параметров указатель на объект addressspace, смещение в адресном про-
пространстве, показывающее начало поиска, максимальное количество искомых
страниц и указатель на массив дескрипторов страниц, который функция
должна заполнить. При выполнении поиска функция f indgetpages () опира-
опирается на функцию radixtreegangiookup (), которая заполняет массив указа-
телей и возвращает количество найденных страниц. Возвращенные страницы
упорядочены по возрастанию индексов, хотя не исключено, что индексы идут
не подряд, поскольку некоторых страниц может и не быть в кэше.
Существует ряд других функций, выполняющих поиск в кэше страниц. На-
Например, функция f indlockpage () аналогична функции f indgetpage (), НО
она увеличивает счетчик обращений возвращенной страницы и вызывает
функцию lockpage (), чтобы установить флаг PGiocked. Таким образом, ко-
когда функция возвратит управление, страница будет доступна только вызвав-
вызвавшему процессу. Функция lockpageO приостанавливает текущий процесс,
если страница уже заблокирована. С этой целью она вызывает функцию
waitonbitiock () для бита PGiocked. Эта последняя функция переводит
текущий процесс в состояние taskuninterruptible, сохраняет дескриптор
процесса в очереди ожидания, выполняет метод syncpage объекта
addressspace, чтобы откупорить очередь запросов блочного устройства, со-
содержащего файл, и вызывает schedule о, чтобы приостановить процесс, пока
у страницы не будет сброшен флаг PGiocked. Чтобы разблокировать страни-
страницу и возобновить выполнение процесса, находящегося в очереди ожидания,
ядро вызывает функцию unlock_page ().
Функция f indtrylockpage () аналогична функции f indlockpage (), НО она
никогда не блокирует процесс. Если запрошенная страница заблокирована,
функция возвращает код ошибки. Наконец, функция f indorcreatepage ()
вызывает f indlockpage (), а если страница не будет найдена, функция выде-
выделяет новую страницу и вставляет ее в кэш.
Добавление страницы
Функция addtopagecache() заносит дескриптор новой страницы в кэш
страниц. В качестве параметров она принимает адрес page дескриптора стра-
страницы, адрес mapping объекта addressspace, значение offset, представляющее
индекс страницы внутри адресного пространства, и флаги выделения памяти
gfpmask, используемые при выделении новых узлов базисного дерева. Функ-
Функция выполняет следующие действия:
1. Вызывает функцию radixtreepreioado, которая отключает вытеснение
в ядре и записывает в процессорную переменную radixtreepreioads не-
несколько Структур radixtreenode. Выделение Структур radix_tree_node
происходит за счет кэша slab-аллокатора radixtreenodecachep. Если
функции radixtreepreloadO не удается ВЫДеЛИТЬ структуры radix_tree_
node, фуНКЦИЯ addtopagecache () Завершается С КОДОМ возврата -ENOMEM.
В противном случае, если функция radixtreepreioado завершилась ус-
успешно, вызвавшая ее функция addtopagecache () может быть уверена,
что занесение в кэш нового дескриптора страницы не "провалится" из-за
нехватки памяти, по крайней мере, для файлов размером до 64 Гбайт.
2. Получает спин-блокировку mapping->tree_iock. Обратите внимание, что
вытеснение в ядре уже отключено функцией radixtreepreioadO.
3. Вызывает функцию radixtreeinsert о, чтобы вставить в дерево новый
узел. Эта функция выполняет следующие действия:
• вызывает функцию radix_tree_maxindex(), чтобы ПОЛучИТЬ МакСИ-
мальный индекс, который может быть вставлен в базисное дерево при
его текущей высоте. Если индекс новой страницы не может быть
представлен в дереве такой высоты, функция вызывает radix_tree_
extend (), чтобы увеличить высоту дерева за счет добавления необхо-
необходимого количества узлов (например, применительно к базисному де-
дереву, изображенному на рис. 15.1, а, функция radix_tree_extend () до-
добавит еще один узел сверху). Новые узлы выделяются функцией
radixtreenodeaiioco, которая пытается получить структуру
radixtreenode из кэша slab-аллокатора или, если не удастся, из пула
заранее выделенных структур, хранящихся в radixtreepreioads;
• начиная с корня (mapping->page_tree), функция совершает обход дере-
дерева в соответствии с индексом страницы offset, пока не достигнет
нужного узла, как описано в предыдущем разделе. Если потребуется,
она выделит новые промежуточные узлы, вызывая функцию
radix_tree_node_alloc()j
• сохраняет дескриптор страницы в соответствующем слоте последнего
пройденного ею узла базисного дерева и возвращает 0.
4. Увеличивает счетчик обращений page->_count дескриптора страницы.
5. Поскольку это новая страница, ее содержимое не действительно. Функ-
Функция устанавливает флаг PGiocked страничного кадра, чтобы защитить
страницу от обращения со стороны других управляющих трактов ядра.
6. Инициализирует поля page->mapping и page->index значениями парамет-
параметров mapping И offset.
7. Увеличивает счетчик кэшированных страниц в адресном пространстве
(mapping->nrpages).
8. Освобождает спин-блокировку адресного пространства.
9. Вызывает radixtreepreioadendo, чтобы опять включить вытеснение
в ядре.
10. Возвращает 0 (успех).
Удаление страницы
Функция removef rompagecache () удаляет дескриптор страницы из кэша
страниц. Она достигает этого следующим образом:
1. Получает спин-блокировку page->mapping->tree_iock и отключает преры-
вания.
2. Вызывает функцию radixtreedeiete (), чтобы удалить узел из дерева.
Эта функция принимает в качестве параметров адрес корня дерева
(page->mapping->page_tree) и индекс страницы, которую надо удалить. Она
выполняет следующие действия:
• начиная с корня, совершает обход дерева в соответствии с индексом
страницы, пока не достигнет нужного узла, как описано в предыдущем
разделе. При этом она строит массив структур radixtreepath, описы-
описывающих элементы пути от корня до листа, соответствующего удаляе-
удаляемой странице;
• в цикле перебирает узлы, собранные в массиве элементов пути, начиная
с последнего узла, содержащего указатель на дескриптор страницы.
У каждого узла эта функция записывает null в элемент массива слотов,
указывающий на следующий узел (или на дескриптор страницы), и
уменьшает значение поля count. Если это значение дошло до нуля,
функция удаляет узел из дерева и возвращает структуру radix_tree_
node в кэш slab-аллокатора. Затем, на следующем шаге цикла, перехо-
переходит к предыдущему узлу в массиве элементов пути. В противном слу-
случае, если поле count не равно нулю, функция сразу же переходит к сле-
следующему шагу цикла;
• возвращает указатель на дескриптор страницы, который был удален из
дерева.
3. Устанавливает поле page->mapping в значение null.
4. Уменьшает на единицу счетчик кэшированных страниц page->mapping->
nrpages.
5. Освобождает СПИН-блОКИроВКу page->mapping->tree_lock, ВКЛЮЧает Пре-
рывания и завершает работу.
Обновление страницы
Функция readcachepage () обеспечивает хранение в кэше самой последней
версии заданной страницы. Ее параметрами являются:
□ mapping — указатель на объект address_space;
□ index — смещение, задающее нужную страницу;
□ filler— указатель на функцию, которая читает данные этой страницы с
диска (как правило, это функция, реализующая метод readpage адресного
пространства);
□ data — указатель, передаваемый функции filler (обычно равный null).
Далее идет упрощенное описание действий этой функции:
1. Вызывает функцию findgetpage() для проверки наличия страницы в
кэше.
2. Если страницы нет в кэше, функция выполняет следующие дополнитель-
дополнительные действия:
• вызывает aiiocpages (), чтобы выделить новый страничный кадр;
• вызывает addtopagecache (), чтобы занести дескриптор страницы в
кэш;
• вызывает функцию lrucacheaddo, чтобы вставить страницу в неак-
неактивный список давно неиспользуемых страниц этой зоны (см.
разд. "Списки давно неиспользуемых страниц (LRU)" главы 17).
3. На этом шаге страница присутствует в кэше. Функция вызывает
markpageaccessed (), чтобы отметить факт обращения к странице.
4. Если страница устарела (флаг PGuptodate сброшен), функция вызывает
функцию filler для чтения страницы с диска.
5. Возвращает адрес дескриптора страницы.
Теги базисного дерева
Как было сказано ранее, кэш страниц не только позволяет ядру быстро обра-
обратиться к странице, содержащей данные из блочного устройства. Он обеспе-
обеспечивает быстрый поиск страниц, находящихся в определенном состоянии.
Предположим, что ядро должно найти в кэше все страницы, принадлежащие
данному владельцу и являющиеся "грязными", т. е. страницы, содержимое
которых еще не было записано на диск. Флаг PGdirty, хранящийся в деск-
дескрипторе страницы, говорит о том, "грязная" ли она. Однако обход всего ба-
базисного дерева для последовательной проверки всех листьев, т. е. дескрипто-
дескрипторов страниц, занял бы недопустимо много времени, если большинство стра-
страниц не являются "грязными".
Чтобы ускорить поиск "грязных" страниц, каждый промежуточный узел в
базисном дереве содержит тег "грязь" для каждого узла-потомка (или листа).
Этот флаг устанавливается тогда и только тогда, когда установлен хотя бы
один тег "грязь" узла-потомка. Теги "грязь" узлов нижнего уровня обычно
являются копиями флагов PGdirty дескрипторов страниц. Таким образом,
когда ядро обходит базисное дерево в поисках "грязных" страниц, оно может
пропустить поддерево с корнем в промежуточном узле, у которого сброшен
тег "грязь". Ядро может быть уверено, что все дескрипторы страниц, содер-
содержащиеся в этом поддереве, не являются "грязными".
Та же идея применяется и в отношении флага PGwriteback, который означа-
означает, что страница в данный момент записывается на диск. Таким образом,
каждый узел базисного дерева размножает два флага дескриптора страницы:
PGdirty и PGwriteback (см. главу 8). Для их хранения каждый узел содержит
два массива из 64 битов в поле tags. Массив tags [0] (или pagecache_
TAG_DIRTY) ЯВЛЯеТСЯ ТеГОМ "грЯЗЬ", а МаССИВ tags[l] (ИЛИ PAGECACHE_TAG_
writeback) является тегом обратной записи.
Функция radixtreetagsetO вызывается при установке флага PGdirty ИЛИ
PGwriteback в кэшированной странице. Она принимает три параметра: ко-
корень базисного дерева, индекс страницы и тип устанавливаемого флага
(pagecache_tag_dirty или pagecache_tag_writeback). Функция совершает об-
обход от корня дерева вниз до листа, соответствующего заданному индексу.
Для каждого узла на пути от корня до листа функция устанавливает тег, ассо-
ассоциированный с указателем на следующий узел пути. Затем она возвращает
адрес дескриптора страницы. В результате каждый узел на пути от корня до
листа помечен должным образом.
Функция radix_tree_tag_clear() вызывается при сбросе флага PGdirty ИЛИ
PGwriteback в кэшированной странице. Она принимает те же параметры, что
и функция radixtreetagset о. Функция совершает обход от корня дерева
вниз до листа, строя массив структур radixtreepath, описывающих путь.
Затем функция идет в обратном направлении, от листа до корня: она сбрасы-
сбрасывает тег узла на самом нижнем уровне, затем проверяет, все ли теги в массиве
этого узла сброшены. Если это так, функция сбрасывает соответствующий
тег в родительском узле на более высоком уровне, проверяет, все ли теги
сброшены у этого узла, и т. д. По окончании работы она возвращает адрес
дескриптора страницы.
Когда дескриптор страницы удаляется из базисного дерева, соответствующие
теги в узлах на пути от корня к листу должны быть обновлены. Функция
radixtreedeiete () делает это корректно (хотя мы и не упомянули об этом
факте в предыдущем разделе). Однако функция radixtreeinsert о не об-
обновляет теги, поскольку предполагается, что у каждого дескриптора страни-
страницы, вставляемого в базисное дерево, флаги PGdirty и PGwriteback сброше-
ны. Впоследствии, если понадобится, ядро сможет вызвать функцию
radix_tree_tag_set().
Функция radixtreetaggedo использует массивы флагов, имеющиеся во
всех узлах дерева, для проверки, содержит ли базисное дерево хотя бы одну
страницу с заданным состоянием. Функция без труда справляется с этим
заданием, выполняя следующий код (root — указатель на структуру radix_
treeroot базисного дерева, a tag — проверяемый флаг):
for (idx = 0; idx < 2; idx++) {
if (root->rnode->tags[tag][idx])
return 1;
}
return 0;
Поскольку можно предположить, что теги во всех узлах базисного дерева об-
обновлены корректно, функции radixtreetaggedo достаточно проверить теги
узла на уровне 1. Эта функция полезна при определении, содержит ли ин-
индексный дескриптор "грязные" страницы, которые следует записать на диск.
Обратите внимание, что на каждом шаге цикла функция проверяет, установ-
установлен ли хотя бы один из 32 флагов, хранящихся в переменной типа unsigned
long.
Функция f indgetpagestag () аналогична функции f indgetpages (), НО она
возвращает только страницы, помеченные с помощью параметра tag. Как мы
увидим в разд. 'Запись "грязных" страниц на диск", эта функция очень важна
для быстрого поиска всех "грязных" страниц индексного дескриптора.
Хранение блоков в кэше страниц
Как было показано в разд. "Управление блочными устройствами" главы 14,
виртуальная файловая система, слой отображения и разнообразные файловые
системы группируют данные на диски в логические единицы, называемые
блоками.
В ранних версиях ядра Linux существовало два основных кэша дисков: кэш
страниц, который хранил целые страницы данных с диска, получавшиеся в
результате обращения к файлам на диске, и кэш буферов, который использо-
использовался для хранения в памяти содержимого блоков, к которым обращалась
виртуальная файловая система при управлении дисковыми файловыми сис-
системами.
Начиная с версии 2.4.10, кэш буферов фактически прекратил свое существо-
существование. По соображениям эффективности, буферы блоков больше не выделя-
выделяются индивидуально. Вместо этого они сохраняются в специальных страни-
страницах, называемых "страницами буферов", которые хранятся в кэше страниц.
Формально страница буфера — это страница данных, ассоциированная с не-
некими дополнительными дескрипторами, называемыми "головами буферов".
Их главное предназначение — быстро находить адрес на диске для каждого
блока страницы. Дело в том, что порции данных, хранящиеся в странице,
принадлежащей кэшу, необязательно являются смежными на диске.
Буферы блоков и головы буферов
У каждого буфера есть дескриптор голова буфера, имеющий тип buf f erhead.
Этот дескриптор содержит всю информацию, необходимую ядру для работы
с блоком, так что перед обработкой блока ядро обязательно проверяет голову
буфера. Поля головы буфера перечислены в табл. 15.4.
Таблица 15.4. Поля головы буфера
Тип Поле Описание
unsigned long b_state Флаги состояния буфера
struct buffer_head * b_this_page Указатель на следующий элемент
списка в странице буферов
struct page * b_page Указатель на дескриптор страницы
буферов, содержащей этот блок
atomic_t b_count Счетчик обращений к блоку
u32 b_size Размер блока
sector_t b_blocknr Номер блока в блочном устройстве
(логический номер блока)
char * b_data Положение буфера внутри страницы
буферов
struct block_device * b_bdev Указатель на дескриптор блочного
устройства
bh_end_io__t * b_end_io Метод завершения ввода/вывода
void * b_private Указатель на данные для метода
завершения ввода/вывода
struct list_head b_assoc_buffers Указатели, используемые в списке
косвенных блоков, ассоциированных
с индексным дескриптором
Два поля головы буфера кодируют адрес блока на диске: поле bbdev иденти-
идентифицирует блочное устройство (обычно диск или раздел), содержащее блок
(см. разд. "Блочные устройства" главы 14), а поле bbiocknr хранит логине-
ский номер блока, т. е. индекс блока внутри диска или раздела.
Поле bdata задает позицию буфера блока внутри страницы буферов. Факти-
Фактически кодирование этой позиции зависит от того, находится ли страница в
верхней памяти. Если находится, поле bdata содержит смещение буфера
блока от начала страницы; в противном случае в этом поле хранится линей-
линейный адрес буфера блока.
Поле bstate может хранить несколько флагов. Некоторые из них находятся в
общем пользовании и перечислены в табл. 15.5. Кроме того, любая файловая
система может определить собственные флаги головы буфера.
Таблица 15.5. Общие флаги головы буфера
Флаг Описание
BHjjptodate Установлен, если буфер содержит осмысленные данные
BH_Dirty Установлен, если буфер является "грязным", т. е. содержит данные,
которые должны быть записаны на блочное устройство
вн_ьоск Установлен, если буфер заблокирован, что обычно имеет место,
когда буфер вовлечен в операцию ввода/вывода
BH_Req Установлен, если пересылка данных для инициализации буфера
уже была запрошена
BH_Mapped Установлен, если буфер отображен на диск, т. е. если поля b_bdev и
b_blocknr соответствующей головы буфера содержат осмысленные
значения
BH_New Установлен, если соответствующий блок только что выделен,
и к нему не было ни одного обращения
BH_Async_Read Установлен, если буфер читается асинхронно
BH_Async_write Установлен, если буфер записывается асинхронно
BHjDelay Установлен, если буфер еще не был выделен на диске
BH_Boundary Установлен, если блок, следующий за данным, не является
смежным с ним
BH_Write_Eio Установлен, если при записи данного блока возникла ошибка
ввода/вывода
BH_Ordered Установлен, если блок должен быть записан строго после блоков,
отправленных до него (используется в журналируемых файловых
системах)
BH_Eopnotsupp Установлен, если блочное устройство не поддерживает
запрошенную операцию
Работа с головами буферов
Головы буферов имеют собственный кэш slab-аллокатора, дескриптор кото-
которого kmemcaches хранится В переменной bhcachep. Функции allocbuf fer_
head () И f reebuf f erhead () ИСПОЛЬЗуЮТСЯ, соответственно, ДЛЯ ВЫДелеНИЯ И
освобождения головы буфера.
Поле bcount головы буфера является счетчиком обращений для соответст-
соответствующего буфера блока. Счетчик увеличивается непосредственно перед каж-
каждой операцией над буфером блока и уменьшается сразу после нее. Буферы
блоков, хранящиеся в кэше страниц, просматриваются периодически, а также
при нехватке памяти, и только буферы с нулевыми счетчиками обращений
могут быть утилизированы (см. главу 17).
Когда управляющий тракт ядра захочет обратиться к буферу блока, он будет
должен вначале увеличить счетчик обращений. Функция, которая находит
блок внутри кэша страниц ( getbiko, см. разд. "Поиск блоков в кэше стра-
страниц" далее в этой главе), делает это автоматически. Поэтому функции высо-
высокого уровня, как правило, не увеличивают счетчик обращений буфера блока.
Когда управляющий поток ядра прекращает работу с буфером блока, он дол-
должен вызвать либо breiseo, либо bforget (), чтобы уменьшить счетчик
обращений. Разница между этими двумя функциями заключается в том, что
bf orget (), кроме всего прочего, удаляет блок из всех списков косвенных
блоков (поле bassocbuf fers головы буфера) и помечает буфер как "чистый",
заставляя ядро забыть про все изменения в буфере, который еще должен быть
записан на диск.
Страницы буферов
Когда ядро должно обратиться к отдельному блоку, оно обращается к стра-
странице буферов, содержащей этот буфер, и проверяет голову буфера.
Ядро создает страницы буферов в следующих двух случаях:
□ при чтении и записи страниц файла, которые хранятся не в смежных бло-
блоках диска. Это происходит либо потому, что файловая система разместила
файл не в смежных блоках, либо потому, что файл содержит "дыры" (см.
разд. "Дыры в файлах" главы 18);
П при обращении к одному блоку на диске (например, при чтении супербло-
суперблока или блока индексного дескриптора).
В первом случае дескриптор страницы буферов вставляется в базисное дере-
дерево обычного файла. Головы буферов сохраняются, потому что содержат цен-
ценную информацию: блочное устройство и номер блока, определяющего поло-
положение данных на диске. В главе 16 показано, как ядро использует страницу
буферов этого типа.
Во втором случае дескриптор страницы буферов вставляется в базисное де-
дерево, корень которого находится в объекте addressspace индексного деск-
дескриптора в специальной файловой системе bdev, ассоциированной с блочным
устройством. Страницы буферов этого типа должны удовлетворять строгому
ограничению: все буферы блоков относятся к смежным блокам блочного уст-
устройства.
В качестве примера рассмотрим ситуацию, в которой виртуальная файловая
система пытается прочитать 1024-байтовый блок индексного дескриптора,
содержащий индексный дескриптор некоторого файла. Вместо выделения
одного буфера ядро должно выделить целую страницу с четырьмя буферами.
Эти буферы будут содержать данные из четырех смежных блоков блочного
устройства, включая запрошенный блок индексного дескриптора.
В этой главе мы уделим основное внимание страницам буферов второго типа,
так называемым страницам буферов блочных устройств.
Все буферы блоков внутри одной страницы буферов должны иметь одинако-
одинаковый размер. Следовательно, в архитектуре 80x86 страница буферов может
включать в себя от одного до восьми буферов, в зависимости от размера
блока.
Когда некоторая страница выступает в качестве страницы буферов, все голо-
головы буферов блоков, хранящиеся в ней, организованы в однонаправленный
циклический список. Поле private дескриптора страницы буферов указывает
на голову буфера первого блока в странице2, а каждая голова буфера содер-
содержит в поле bthispage указатель на следующую голову буфера в списке.
Рис. 15.2. Страница буферов с четырьмя буферами и их головы
2 Поскольку поле private содержит осмысленные данные, флаг PG_private этой страницы уста-
установлен. Следовательно, если некая страница содержит данные с диска, и у нее установлен флаг
PG_private, то она является страницей буферов. Заметим, что другие компоненты ядра, не имею-
имеющие отношения к подсистеме ввода/вывода, используют поля private и PG_private для других
целей.
Кроме того, каждая голова буфера содержит адрес дескриптора страницы бу-
буферов в поле bpage. На рис. 15.2 изображена страница буферов с четырьмя
буферами блоков и соответствующими головами буферов.
Выделение страниц буферов
блочных устройств
Ядро выделяет новую страницу буферов блочного устройства, когда оно об-
обнаруживает, что в кэше страниц нет страницы, содержащей буфер для задан-
заданного блока (см. разд. "Поиск блоков в кэше страниц" далее в этой главе).
В частности, операция поиска блока может закончиться неуспешно по одной
из следующих причин:
□ Базисное дерево блочного устройства не содержит страницы с данными
этого блока. В этом случае к базисному дереву должен быть добавлен
новый дескриптор страницы.
□ Базисное дерево блочного устройства содержит страницу с данными этого
блока, но она не является страницей буфера. В этом случае необходимо
создать новые головы буферов и связать их с этой страницей, тем самым
преобразовав ее в страницу буферов блочного устройства.
□ Базисное дерево блочного устройства содержит страницу буферов с дан-
данными этого блока, но она разбита на блоки, размер которых отличается от
размера запрошенного блока. В этом случае старые головы буферов долж-
должны быть освобождены, и новый набор голов буферов должен быть создан
и связан со страницей.
Чтобы добавить страницу буферов блочного устройства в кэш страниц, ядро
вызывает функцию grow_buf fers (), которая принимает три параметра, иден-
идентифицирующие блок:
□ адрес bdev дескриптора block_device;
□ логический номер блока block, т. е. позицию блока в блочном устройстве;
□ размер блока size.
Функция выполняет следующие действия:
1. Вычисляет смещение index страницы данных в пределах блочного устрой-
устройства, содержащего запрошенный блок.
2. Вызывает функцию growdevpage (), чтобы создать новую страницу буфе-
буферов блочного устройства, если это необходимо. Вызванная функция вызы-
вызывает функцию f indorcreatepage (), передавая ей объект addressspace
блочного устройства (bdev->bd_inode->i_mapping), смещение в странице
index и флаг gfpnofs. Как было сказано ранее, в разд. "Функции работы
с кэшем страниц", функция findor create page о ищет страницу в кэше
и, если необходимо, вставляет в кэш новую страницу.
3. Итак, требуемая страница находится в кэше, и у функции есть адрес ее
дескриптора. Тогда функция проверяет флаг PGprivate. Если он равен
null, значит, страница еще не является страницей буферов (с ней не ассо-
ассоциированы никакие головы буферов). Функция f indorcreatepage () пе-
переходит на шаг 6.
4. Страница является страницей буферов. Функция извлекает из поля
private дескриптора страницы адрес bh первой головы буфера и проверя-
проверяет, равен ли размер блока bh->size размеру запрошенного блока. Если это
так, значит, страница, найденная в кэше, является допустимой страницей
буферов. Функция f indorcreatepage () переходит на шаг 8.
5. Страница содержит блоки не того размера. Функция вызывает try_to_
freebuf fers о, чтобы освободить имеющиеся головы буферов в странице
буферов.
6. f ind_or_create_page () вызывает функцию alloc_page_buf fers (), чтобы
разместить головы буферов для блоков необходимого размера в пределах
страницы и занести их в однонаправленный циклический список, органи-
организованный с помощью полей bthispage. Кроме этого, функция записыва-
ет в поля bpage голов буферов адрес дескриптора страницы, а в поля
bdata — смещение или линейный адрес буфера внутри страницы.
7. findorcreatepage() сохраняет адрес первой головы буфера в поле
private, устанавливает поле PGprivate и увеличивает счетчик обращений
страницы (считается, что буферы блоков внутри страницы обращаются к
странице).
8. f ind_or_create_page () вызывает функцию init_page_buf fers () ДЛЯ ИНИ-
циализации полей bbdev, bbiocknr и bbstate голов буферов, связанных
со страницей. Все блоки расположены на диске смежно, значит, их логи-
логические номера идут последовательно, и их легко вычислять на основании
параметра block.
9. f indorcreatepage () возвращает адрес дескриптора страницы.
10. Функция growbuf f ers () снимает блокировку со страницы (была забло-
заблокирована функцией f indorcreatepage () ).
11. Затем она уменьшает счетчик обращений (счетчик был увеличен все той
же функцией f ind_or_create_page ()).
12. Функция growbuf fers о возвращает 1 (успех).
Освобождение страниц
буферов блочных устройств
Как мы увидим в главе 17, страницы буферов блочных устройств освобожда-
освобождаются, когда ядро пытается получить в свое распоряжение дополнительную
свободную память. Очевидно, что страницу нельзя освободить, если она со-
содержит "грязные" или заблокированные буферы. Чтобы освободить страницы
буферов, ядро вызывает функцию trytoreieasepage (), которая принимает
адрес page дескриптора страницы и выполняет следующие действия3:
1. Если флаг PGwriteback установлен, функция возвращает 0 (освобождение
невозможно, поскольку страница в этот момент записывается обратно на
диск).
2. Вызывает метод reieasepage объекта addressspace данного блочного уст-
устройства, если этот метод определен. (Как правило, для блочных устройств
он не определен.)
3. Вызывает функцию try_to_free_buffers () и выдает код возврата, полу-
полученный от нее.
Что касается функции trytof reebuf f ers (), она перебирает головы буфе-
буферов, связанные со страницей, и выполняет следующие действия:
1. Проверяет флаги всех голов буферов, входящих в страницу. Если у какой-
то головы буфера установлен флаг BHDirty или BHLocked, функция за-
завершает работу, возвращая 0 (неудача), поскольку освободить буферы не-
невозможно.
2. Если голова буфера находится в списке косвенных буферов (см.
разд. "Блочные буферы и головы буферов" ранее в этой главе), функция
удаляет ее из списка.
3. Сбрасывает флаг PGprivate дескриптора страницы, записывает в поле
private значение null и уменьшает счетчик обращений страницы.
4. Сбрасывает флаг страницы PGdirty.
5. Многократно ВЫЗЫВаеТ фуНКЦИЮ free_buffer_head(), Чтобы ОСВОбоДИТЬ
все головы буферов этой страницы.
6. Возвращает 1 (успех).
Поиск блоков в кэше страниц
Когда ядру нужно прочитать или записать один блок физического устройства
(например, суперблок), оно должно проверить, находится ли в кэше страниц
3 Функция try_to_release_page () может быть также вызвана для страниц буферов, принадле-
принадлежащих обычным файлам.
буфер нужного блока. Поиск в кэше заданного буфера (определяемого адре-
адресом bdev дескриптора блочного устройства и логическим номером блока пг)
состоит из трех шагов:
1. Получение указателя на объект addressspace блочного устройства, со-
содержащего блок (bdev->bd_inode->i_mapping).
2. Получение размера блока устройства (bdev->bd_biock_size) и вычисление
индекса страницы, содержащей блок. Это всегда сводится к операции по-
побитового сдвига логического номера блока. Например, если размер блока
равен 1024 байтов, каждая страница буферов содержит четыре буфера
блоков. Следовательно, индекс страницы равен пг/4.
3. Поиск страницы буферов в базисном дереве блочного устройства. После
получения дескриптора страницы ядро имеет доступ к головам буферов,
описывающим состояние буферов блоков в странице.
Впрочем, в действительности, все чуть сложнее. Для повышения производи-
производительности системы ядро имеет дело с массивом bhirus, состоящим из не-
небольших кэшей диска, по одному на каждый процессор.
Каждый такой кэш, называемый кэшем давно неиспользуемых блоков, содер-
содержит восемь указателей на головы буферов, к которым обращался данный
процессор за последнее время. Элементы каждого массива указателей отсор-
отсортированы так, что указатель на голову буфера, которая была использована
позже других, имеет индекс 0. Одна и та же голова буфера может находиться
в нескольких массивах указателей разных процессоров (но не может встре-
встретиться дважды в одном массиве указателей одного процессора). Для каждого
вхождения головы буфера в такой кэш счетчик обращений к ней bcount уве-
увеличивается на единицу.
Функция find_get_block()
Функция findgetbiock() принимает в качестве параметров адрес bdev
дескриптора biockdevice, номер блока block и размер блока size. Она воз-
возвращает адрес головы буфера, связанной с буфером блока в кэше страниц,
или null, если такого буфера блока нет. Функция выполняет следующие
действия:
1. Проверяет кэш давно не используемых блоков, соответствующий данному
процессору. Точнее говоря, она проверяет, содержит ли этот кэш голову
буфера, у которой поля bbdev, bbiocknr и bsize, соответственно, равны
значениям параметров bdev, block и size.
2. Если голова буфера присутствует в кэше, функция переупорядочивает
элементы массива так, чтобы поместить указатель на только что обнару-
женную голову буфера на первое место (с индексом 0). Затем она увели-
увеличивает значение в поле bcount и переходит к шагу 8.
3. На этом шаге функция оказывается, если головы буфера нет в кэше.
Функция вычисляет по номеру и размеру блока индекс страницы по отно-
отношению к блочному устройству.
index = block » (PAGE_SHIFT - bdev->bd_inode->i_blkbits);
4. Вызывает find_get_page(), чтобы найти в кэше страниц дескриптор стра-
страницы буферов, содержащей требуемый буфер блока. В качестве парамет-
параметров вызываемой функции передаются указатель на объект addressspace
блОЧНОГО устройства (bdev->bd_inode->i_mapping) И ИНДеКС СТраНИЦЫ, ЧТО-
бы найти в кэше страниц дескриптор страницы буферов, содержащей тре-
требуемый буфер блока. Если в кэше нет такой страницы, функция возвраща-
возвращает null (неудача).
5. На этом шаге у функции есть адрес дескриптора страницы буферов. Она
перебирает список голов буферов, связанных со страницей буферов, пыта-
пытаясь найти блок с логическим номером, равным block.
6. Уменьшает поле count дескриптора страницы (оно было увеличено функ-
функцией f ind_get_page ()).
7. Сдвигает все элементы в кэше давно не используемых блоков на одну по-
позицию вниз и ставит на первое место указатель на голову буфера запро-
запрошенного блока. Если какая-либо голова буфера выпала из кэша давно не-
неиспользуемых блоков, функция уменьшает ее счетчик обращений bcount.
8. Вызывает функцию markpageaccessedo, чтобы перенести страницу бу-
буферов в соответствующий список давно неиспользуемых страниц, если это
необходимо (см. разд. "Списки давно неиспользуемых страниц (LRU)"
главы 17).
9. Возвращает указатель на голову буфера.
Функция getblkQ
Функция getbiko принимает те же параметры, что и f indgetbiock (), а
именно: адрес bdev дескриптора biockdevice, номер блока block и размер
блока size. Она возвращает адрес головы буфера. Эта функция никогда не
заканчивается неуспешно: даже если блок не существует, она любезно выде-
выделяет страницу буферов для блочного устройства и возвращает указатель на
голову буфера, которая должна описывать блок. Обратите внимание, что
буфер блока, возвращенный функцией getbiko, не обязательно содер-
содержит осмысленные данные; флаг BHUptodate головы буфера может быть
сброшен.
Функция getbik () выполняет следующие действия:
1. Вызывает find_get_biock(), чтобы проверить наличие блока в кэше
страниц. Если блок найден, функция возвращает адрес головы его буфера.
2. В противном случае функция вызывает growbuffers о, чтобы выделить
новую страницу буферов для запрошенного блока (см. разд. "Выделение
страниц буферов блочных устройств"ранее в этой главе).
3. Если функции growbuffers о не удается выделить такую страницу, функ-
функция getbik о пытается утилизировать какое-то количество памяти, вы-
вызвав f ree_rnore_memory () (см. главу 17).
4. Возвращается на шаг 1.
Функция breadQ
Функция bread () принимает те же параметры, что и f indgetbiock (), а
именно: адрес bdev дескриптора biockdevice, номер блока block и размер
блока size. Она возвращает адрес головы буфера. В отличие от getbik о,
данная функция, прежде чем возвратить голову буфера, читает блок с диска,
если это необходимо. Функция bread () выполняет следующие действия:
1. Вызывает функцию getbik о, чтобы найти в кэше страниц страницу бу-
буферов, ассоциированную с требуемым блоком, и получить указатель на
соответствующую голову буфера.
2. Если блок уже присутствует в кэше страниц, и буфер содержит осмыслен-
осмысленные данные (флаг BHUptodate установлен), функция возвращает адрес го-
головы буфера.
3. В противном случае она увеличивает счетчик обращений головы буфера.
4. Записывает В ПОЛе b_end_io адрес end_buf f er_read_sync ().
5. Вызывает функцию submitbh (), чтобы передать голову буфера общему
слою работы с блочными устройствами.
6. Вызывает функцию waitonbuf f ег (), чтобы поместить текущий процесс в
очередь, пока операция ввода/вывода не будет завершена, т. е. пока флаг
внъоск головы буфера не будет сброшен.
7. Возвращает адрес головы буфера.
Передача голов буферов общему слою работы
с блочными устройствами
Пара функций, submitbho и lirwbiocko, позволяет ядру запустить опера-
операцию ввода/вывода для одного или нескольких буферов, описываемых их го-
головами.
Функция submit_bh()
Чтобы передать общему слою работы с блочными устройствами одну голову
буфера и тем самым запросить передачу одного блока данных, ядро вызывает
функцию submitbh (). Ее параметрами являются направление движения дан-
данных (read или write) и указатель bh на голову буфера, описывающую буфер
блока.
Функция submitbho предполагает, что голова буфера полностью инициали-
инициализирована. В ЧаСТНОСТИ, ПОЛЯ b_bdev, bblocknr И bsize ДОЛЖНЫ ИДеНТИфиЦИ-
ровать блок на диске, содержащий запрошенные данные. Если буфер блока
принадлежит странице буферов блочного устройства, инициализация головы
буфера выполняется функцией f indgetbiock (), как описано в предыду-
предыдущем разделе. Однако, как мы увидим в следующей главе, функция
submitbho может быть также вызвана для блоков, принадлежащих страни-
страницам буферов, которыми владеют обычные файлы.
Функция submitbh () представляет собой нечто большее, чем просто "склеи-
"склеивающая" функция, которая создает запрос bio на основании содержимого го-
головы буфера и затем вызывает generic_make_request () (см. разд. "Выдача за-
запроса'1 главы 14). Основные действия, выполняемые ею, таковы:
1. Устанавливает флаг BHReq головы буфера, чтобы отметить, что блок был
передан, как минимум, один раз. Кроме того, если направление движения
ДаННЫХ WRITE, фуНКЦИЯ Сбрасывает флаг BH_Write_EIO.
2. Вызывает bioaiiocO для выделения нового дескриптора bio (см. гла-
главу 14).
3. Инициализирует поля дескриптора bio в соответствии с содержимым го-
головы буфера:
• записывает в поле bisector номер первого сектора в блоке (bh->
b_blocknr * bh->b_size / 512);
• записывает в поле bibdev адрес дескриптора блочного устройства
(bh->b_bdev);
• записывает в поле bi size размер блока (bh->b_size);
• инициализирует первый элемент массива biiovec так, чтобы сегмент
соответствовал буферу блока. В bi_io_vec[0] .bvpage записывается
bh->b_page, В bi_io_vec[0] .bv_len записывается bh->b_size, а В bi_io_
vec [0] .bvof fset записывается смещение буфера блока в странице, ука-
указанное В bh->b_data;
• записывает единицу в bivcnt (только один сегмент в bio) и ноль в
biidx (передается текущий сегмент);
• записывает в поле biendio адрес endbiobhiosync(), а в поле
biprivate — адрес головы буфера. Функция будет вызвана, когда пе-
пересылка данных завершится.
4. Увеличивает счетчик ссылок bio (он становится равным 2).
5. Вызывает функцию submitbio (), которая устанавливает флаг birw в со-
соответствии с направлением пересылки данных, обновляет переменную
pagestates (свою у каждого процессора), чтобы отслеживать количество
прочитанных и записанных секторов, и вызывает функцию generic_
makerequest () ДЛЯ дескриптора bio.
6. Уменьшает счетчик обращений к bio.
7. Дескриптор bio не освобождается, потому что он заносится в очередь пла-
планировщика ввода/вывода.
8. Возвращает 0 (успех).
Когда ввод/вывод bio заканчивается, ядро вызывает метод biendio. В дан-
данном случае это функция endbiobhiosync (). Она читает адрес головы бу-
буфера из поля biprivate структуры bio, затем вызывает метод bendio голо-
головы буфера (он был корректно установлен до вызова функции submitbh ()) и,
наконец, вызывает bioput (), чтобы уничтожить структуру bio.
Функция ll_rw_block()
Иногда ядро должно запустить пересылку сразу нескольких блоков данных,
которые необязательно являются физически смежными. Функция lirwbiocko
принимает в качестве параметров направление движения данных (read или
write), количество блоков, подлежащих пересылке, и массив указателей на
головы соответствующих буферов блоков. Функция перебирает все головы
буферов и для каждой выполняет следующие действия:
1. Проверяет и устанавливает флаг вньоск головы буфера. Если буфер уже
был заблокирован, значит, пересылка данных была активизирована другим
управляющим трактом ядра. В этом случае функция пропускает буфер,
переходя к шагу 9.
2. Увеличивает на единицу счетчик обращений bcount головы буфера.
3. Если направлением движения данных является write, функция настраива-
настраивает метод bendio головы буфера так, чтобы он указывал на адрес функции
endbuf f erwritesync (). В противном случае она настраивает этот метод
так, чтобы он указывал на адрес функции endbuf f erreadsync ().
4. Если направлением движения данных является write, функция проверяет и
сбрасывает флаг BHDirty головы буфера. Если флаг не был установлен,
нет необходимости записывать блок на диск, и функция переходит
к шагу 7.
5. Если направлением движения данных является read или reada (опере-
(опережающее чтение), функция проверяет, установлен ли флаг BHUptodate го-
головы буфера. Если установлен, нет необходимости читать блок с диска, и
функция переходит к шагу 7.
6. На этом шаге блок должен быть прочитан или записан. Функция вызывает
submitbh (), чтобы передать голову буфера общему слою работы с блоч-
блочными устройствами, и переходит к шагу 9.
7. Снимает блокировку с головы буфера, сбросив флаг вньоск, и возобнов-
возобновляет работу каждого процесса, ждущего разблокирования блока.
8. Уменьшает значение поля bcount головы буфера.
9. Если в массиве есть еще головы буферов, функция выбирает следующую и
возвращается к шагу 1; в противном случае она завершает работу.
Обратите внимание, что если функция lirwbiocko передает голову буфера
общему слою работы с блочными устройствами, она оставляет буфер забло-
заблокированным, а его счетчик ссылок увеличенным, чтобы буфер оставался не-
недоступным и не мог быть освобожден, пока не завершится пересылка дан-
данных. Ядро выполняет завершающий метод bendio головы буфера, когда
окончится пересылка блока. Если не было ошибки ввода/вывода, функции
end_buf f er_write_sync () И end_buf f er_read_sync () просто устанавливают
флаг в поле BHUptodate головы буфера, снимают с буфера блокировку и
уменьшают его счетчик обращений.
Запись грязных страниц на диск
Как мы уже знаем, ядро заполняет кэш страниц страницами, содержащими
данные блочных устройств. Когда процесс модифицирует какие-либо дан-
данные, соответствующая страница помечается как грязная, т. е. устанавливается
флаг PG_dirty.
В системах Unix допускается отложенная запись грязных страниц на блочные
устройства, потому что это заметно увеличивает производительность систе-
системы. Несколько операций записи над страницей в кэше могут быть удовлетво-
удовлетворены единственным физическим обновлением соответствующих секторов
диска. Кроме того, операции записи менее критичны, чем операции чтения,
поскольку процесс обычно не приостанавливается, если запись отложена, а
при отложенной операции чтения он, скорее всего, будет блокирован. Благо-
Благодаря отложенным операциям записи каждое физическое блочное устройство
обслужит, в среднем, гораздо больше запросов на чтение, чем на запись.
Грязная страница, в принципе, может оставаться в основной памяти до само-
самого последнего момента, т. е. до выключения системы. Однако такая край-
ность при определении стратегии откладывания записи имеет два основных
недостатка:
□ если произойдет сбой питания, содержимое оперативной памяти будет
невозможно восстановить, и многие обновления файлов, сделанные с мо-
момента загрузки системы, будут потеряны;
□ размер кэша страниц и, следовательно, объем оперативной памяти, необ-
необходимой для его хранения, может оказаться очень большим, по меньшей
мере, таким же, как и размер блочных устройств, к которым происходили
обращения.
Поэтому грязные страницы принудительно сбрасываются (записываются) на
диск в следующих случаях:
□ кэш страниц переполняется, и необходимы новые страницы, либо количе-
количество грязных страниц становится слишком большим;
□ страница пребывает в "грязном" состоянии слишком много времени;
□ процесс требует сброса на диск всех "висящих" обновлений или какого-то
конкретного файла. Для этого он делает системный вызов sync (), f sync ()
или f datasync () (см. разд. "Системные вызовы syncQ, fsyncQ ufdatasyncQ "
далее в этой главе).
Страницы буферов еще больше усложняют ситуацию. Головы буферов, ассо-
ассоциированные с каждой страницей буферов, позволяют ядру отслеживать со-
состояние каждого буфера в отдельности. Флаг PGdirty страницы буферов
должен быть установлен, если хотя бы у одной из ассоциированных голов
буферов установлен флаг BHDirty. Когда ядро выбирает грязную страницу
буферов для сброса на диск, оно просматривает ассоциированные головы бу-
буферов и записывает на диск только содержимое "грязных" блоков. Как только
ядро запишет на диск все "грязные" блоки страницы буферов, оно сбрасывает
флаг PGdirty этой страницы.
Потоки ядра pdflush
В ранних версиях Linux существовал поток ядра, называемый bdflush, кото-
который систематически сканировал кэш страниц в поисках грязных страниц.
Существовал и второй поток ядра, kupdate, для гарантии того, что никакая
страница не останется грязной слишком долго. В Linux 2.6 оба потока заме-
заменены группой потоков общего назначения, названных pdflush.
Эти потоки ядра имеют гибкую структуру. Они принимают два параметра:
указатель на функцию, которую нужно выполнить, и параметр для этой
функции. Количество потоков ядра pdflush в системе регулируется динамиче-
динамически. Когда потоков слишком мало, создаются новые, а когда их слишком
много, они уничтожаются. Поскольку функции, выполняемые этими потока-
ми ядра, могут быть блокирующими, создание нескольких потоков pdflush
вместо одного увеличивает производительность системы.
Рождение и гибель потоков регулируются следующими правилами:
□ в системе должно работать минимум два и максимум восемь потоков
pdflush;
□ если в течение последней секунды не было ни одного простаивающего
потока pdflush, должен быть создан новый поток;
□ если последний поток pdflush простаивает более одной секунды, он дол-
должен быть уничтожен.
Каждый поток ядра pdflush имеет дескриптор pdfiushwork (табл. 15.6). Деск-
Дескрипторы простаивающих потоков заносятся в список pdfiushiist, а спин-
блокировка pdf lushiock защищает этот список от одновременного обраще-
обращения в многопроцессорных системах. Переменная nrpdf lushthreads4 содер-
содержит общее количество потоков pdflush (как свободных, так и занятых), а пе-
переменная lastemptyjifs содержит время (в тиках) последнего опустошения
списка pdf lushiist потоков ядра pdflush.
Таблица 15.6. Поля дескриптора pdf lushwork
Тип Поле Описание
struct task_struct * who Указатель на дескриптор
потока ядра
void(*) (unsigned long) fn Функция обратного вызова,
которую должен выполнить
поток ядра
unsigned long argO Аргумент функции обратного
вызова
struct list head list Указатели, используемые в
списке pdf lush_list
unsigned long when_i_went_to_sleep Время в тиках, когда поток ядра
стал доступен
Каждый поток ядра pdflush выполняет функцию pdflush (), которая содер-
содержит бесконечный цикл, работающий, пока поток ядра не будет уничтожен.
Предположим, что поток pdflush простаивает. Тогда процесс приостанов-
приостановлен и находится в состоянии taskinterruptible. Как только поток ядра
"пробуждается", функция pdflush о обращается к своему дескриптору
pdf lushwork и выполняет функцию обратного вызова, хранящуюся в поле fn,
4 Значение этой переменной может быть прочитано из файла /proc/sys/vm/nr_pdflush_threads.
передав ей аргумент, который хранится в поле argO. Когда функция обратно-
обратного вызова завершает работу, функция pdf lush () проверяет значение пере-
переменной lastemptyjifs. Если в течение более одной секунды не было про-
простаивающих потоков pdf lush, и общее количество потоков меньше восьми,
функция pdf lush о запускает еще один поток. В противном случае, если
последний элемент в списке pdf lushiist простаивает более одной секунды,
и существует более двух потоков ядра pdf lush, функция pdf lush () завер-
завершает работу. Как было сказано в разд. "Kernel Threads" главы 3, соответст-
соответствующий поток ядра делает системный вызов exit () и тем самым уничтожа-
уничтожается. В противном случае функция pdf lush () заново вставляет дескриптор
pdfiushwork потока ядра в список pdfiushiist и переводит поток ядра в
состояние "сна".
Функция pdf lushoperation () используется для активизации простаивающего
потока ядра pdflush. Она принимает два параметра: указатель f n на функцию,
которая должна быть выполнена, и аргумент argO. Функция pdfiush_
operation () делает следующее:
1. Извлекает из списка pdfiushiist указатель pdf на дескриптор
pdf lushwork простаивающего потока pdflush. Если список пуст, она воз-
возвращает-1. Если список состоял только из одного элемента, функция за-
записывает в переменную lastemptyjifs значение jiffies.
2. Сохраняет в pdf->fn и pdf->argo значения параметров fn и argO.
3. Вызывает функцию wakeupprocess (), чтобы "разбудить" простаивающий
поток ядра pdflush (то есть pdf->who).
Какие задания поручаются потокам ядра pdflush? Их не очень много, и все
они имеют отношение к принудительной записи "грязных" данных на диск.
В частности, поток pdflush обычно выполняет одну из следующих функций
обратного вызова:
□ backgroundwriteout() — систематически просматривает кэш страниц в
поисках грязных страниц, которые можно сбросить на диск;
□ wbkupdate () — следит за тем, чтобы никакая страница в кэше не остава-
оставалась "грязной" слишком долго (см. разд. "Запись старых грязных страниц
на диск" далее в этой главе).
Поиск грязных страниц для записи на диск
Каждое базисное дерево может включать в себя грязные страницы, подлежа-
подлежащие записи на диск. Чтобы получить все такие страницы, необходим исчер-
исчерпывающий поиск среди объектов addressspace, ассоциированных с индекс-
индексными дескрипторами, имеющими образ на диске. Поскольку кэш страниц
может содержать большое количество страниц, сканирование всего кэша за
один проход может надолго занять центральный процессор и диски. Поэтому
в системе Linux принят сложный механизм, разбивающий сканирование кэша
страниц на несколько проходов.
Функция wakeupbdf lush () принимает в качестве аргумента количество гряз-
грязных страниц в кэше, подлежащих принудительной записи на диск, причем
ноль означает, что на диск должны быть записаны все грязные страницы кэ-
кэша. Эта функция вызывает функцию pdfiushoperationo, чтобы "разбудить"
поток pdflush и делегирует ей выполнение функции обратного вызова
backgroundwriteout (). Последняя извлекает из кэша страниц указанное ко-
количество грязных страниц и записывает их на диск.
Функция wakeupbdf lush () выполняется либо при нехватке памяти, либо ко-
когда пользователь явно требует операцию сброса страницы на диск. В частно-
частности, эта функция вызывается в следующих случаях:
□ процесс режима пользователя делает системный вызов sync о (см.
разд. "Системные вызовы sync(), fsyncQ ufdatasyncQ " далее в этой главе);
П функции growbuffers() не удается выделить новую страницу буферов
(см. разд. "Выделение страниц буферов блочных устройств"ранее в этой
главе);
П алгоритм утилизации страничных кадров вызывает функцию free_more_
memory () ИЛИ try_to_free_pages () (см. главу 17);
П функции mempooiaiioc () не удается выделить новый элемент пула памяти
(см. главу 8).
Кроме того, поток pdflush, выполняющий функцию обратного вызова
backgroundwriteout (), "пробуждается" каждым процессом, который моди-
модифицирует содержание страниц в кэше и увеличивает долю "грязных" страниц
до значения, превышающего некий фоновый порог. Этот фоновый порог
обычно устанавливается на уровне 10% от общего количества страниц в сис-
системе, но его можно изменить, записав новое значение в файл /proc/sys/vm
/dirty_background_ratio.
Функция backgroundwriteout() использует в своей работе структуру
writebackcontroi, которая действует как коммуникационное устройство.
С ОДНОЙ СТОрОНЫ, ОНа Сообщает ВСПОМОГатеЛЬНОЙ фуНКЦИИ writeback_inodes (),
что надо сделать, а с другой — содержит статистическую информацию о ко-
количестве страниц, записанных на диск. Назовем самые важные поля этой
структуры:
П syncmode — задает режим синхронизации:
• wbsyncall— при обнаружении заблокированного индексного деск-
дескриптора следует ждать его разблокирования;
• wbsynchold — заблокированные индексные дескрипторы помещаются
в список для дальнейшего рассмотрения;
• wbsyncnone — заблокированные индексные дескрипторы можно про-
просто игнорировать;
□ bdi— если это поле не содержит null, оно указывает на структуру
backingdevinf о. В этом случае на диск будут сброшены только грязные
страницы, принадлежащие соответствующему блочному устройству;
□ olderthanthis — если это поле не содержит null, индексные дескрипто-
дескрипторы, "моложе" заданного значения, должны быть проигнорированы;
□ nrtowrite — количество грязных страниц, еще подлежащих записи на
данном проходе;
□ nonbiocking — если этот флаг установлен, процесс нельзя блокировать.
Функция backgroundwriteout () принимает один параметр, nrpages, задаю-
щий минимальное количество страниц, подлежащих принудительной записи
на диск. Она выполняет следующие действия:
1. Читает из переменной pagestate (имеющейся у каждого процессора) ко-
количество страниц, в том числе грязных, хранящихся в кэше. Если доля
грязных страниц ниже установленного порога, и, по меньшей мере,
nrpages страниц было сброшено на диск, функция завершает работу. Зна-
Значение этого порога обычно составляет 40% от общего количества страниц
в системе, но его можно изменить, записав новое значение в файл
/proc/sy s/vm/dirty_ratio.
2. Вызывает функцию writeback_inodes () в попытке записать на диск
1024 грязных страниц.
3. Проверяет количество фактически записанных страниц и уменьшает коли-
количество страниц, еще подлежащих записи.
4. Если было записано менее 1024 страниц, или если страницы были пропу-
пропущены, это, вероятнее всего, свидетельствует о переполнении очереди за-
запросов блочного устройства.
5. Функция приостанавливает выполнение текущего процесса, помещая его в
специальную очередь на 100 мс, либо до момента, когда очередь запросов
перестанет быть переполненной.
6. Переходит к шагу 1.
Функция writebackinodes() принимает единственный параметр, указатель
wbc на дескриптор writebackcontroi. Поле nrtowrite этого дескриптора
содержит количество страниц, подлежащих сбросу на диск. Когда функция
возвращает управление, это поле содержит количество еще не сброшенных
страниц. Если все прошло гладко, поле содержит ноль.
Предположим, что функция writebackinodes () вызвана при следующих ус-
условиях: указатели wbc->bdi и wbc->oider_than_this равны null, выбран режим
синхронизации wbsyncnone и установлен флаг wbc->nonbiocking (все эти
значения заданы функцией backgroundwriteouto). Тогда функция сканирует
список суперблоков с корнем в переменной superbiocks (см. разд. "Супер-
"Суперблоки" главы 12). Сканирование заканчивается, когда либо просмотрен весь
список, либо достигнуто заданное количество страниц, сбрасываемых на
диск.
Для каждого суперблока sb функция проверяет, пусты ли списки sb->s_dirty
и sb->s_io. Первый из них содержит "грязные" индексные дескрипторы су-
суперблока, а второй — индексные дескрипторы, ожидающие записи на диск.
Если оба списка пусты, значит, индексные дескрипторы в этой файловой сис-
системе не имеют грязных страниц, и функция переходит к следующему супер-
суперблоку.
Допустим, суперблок имеет "грязные" индексные дескрипторы. Функция вы-
вызывает syncsbinodes о для суперблока sb. Вызванная функция:
1. Заносит все индексные дескрипторы из списка sb->s_dirty в список
sb->s_io и очищает список "грязных" индексных дескрипторов.
2. Читает следующий указатель inode из списка sb->s_io. Если список пуст,
возвращает управление.
3. Если индексный дескриптор стал "грязным" после старта функции
syncsbinodes(), она игнорирует грязные страницы этого индексного
дескриптора и возвращает управление. Обратите внимание, что в списке
sb->s_io могут остаться "грязные" индексные дескрипторы.
4. Если текущим процессом является поток ядра pdflush, функция проверяет,
не пытается ли другой поток pdflush, работающий на другом процессоре,
записать грязные страницы файлов, принадлежащих этому блочному уст-
устройству. Это можно сделать с помощью атомарной операции проверки и
установки флага BDI_pdflush структуры backingdevinf о ДанНОГО ИНДекС-
ного дескриптора. В сущности, нет смысла иметь более одного потока яд-
ядра pdflush для одной очереди запросов (см. разд. "Потоки ядра pdflush"
ранее в этой главе).
5. Увеличивает на единицу счетчик обращений индексного дескриптора.
6. Вызывает функцию writebacksingieinodeo, чтобы записать на диск
"грязные" буферы, ассоциированные с выбранным индексным дескрип-
дескриптором:
• если индексный дескриптор заблокирован, функция переносит inode
В СПИСОК "грЯЗНЫХ" ИНДеКСНЫХ ДеСКрИПТОрОВ (inode->i_sb->S_dirty) И
возвращает 0. Поскольку мы предположили, что поле wbc->sync_mode
не равно wbsyncall, функция не ждет разблокирования индексного
дескриптора;
• вызывает метод writepages адресного пространства данного индексно-
индексного дескриптора (или функцию mpagewritepages (), если такого метода
нет), чтобы записать до wbc->nr_to_write "грязных" страниц. Эта
ФУНКЦИЯ ИСПОЛЬЗуеТ фуНКЦИЮ find_get_pages_tag() ДЛЯ быСТрОГО ПО-
лучения всех "грязных" страниц в адресном пространстве индексного
дескриптора (см. разд. "Теги базисного дерева"ранее в этой главе)',
• если индексный дескриптор "грязен", функция вызывает метод супер-
суперблока writeinode, чтобы записать индексный дескриптор на диск.
Функции, реализующие этот метод, обычно используют функцию
submitbh () для пересылки одного блока данных (см. разд. "Передача
голов буферов общему слою работы с блочными устройствами"ранее
в этой главе)',
• проверяет состояние индексного дескриптора. По результатам провер-
проверки переносит индексный дескриптор либо обратно в список sb->
sdirty, если какая-то страница индексного дескриптора еще "гряз-
"грязная", либо в список inodeunused, если счетчик ссылок индексного де-
дескриптора равен нулю, либо в список inodeinuse, если первые два
условия не удовлетворены (см. главу 12);
• ВОЗВращает КОД ошибки функции f indgetpagestag (), вызванной В
начале этого шага.
7. Продолжается выполнение функции sync_sb_inodes(). Если текущим
процессом является поток ядра pdflush, функция сбрасывает флаг
BDipdf lush, установленный на шаге 4.
8. Если в только что обработанном индексном дескрипторе некоторые стра-
страницы были пропущены, значит, он содержит заблокированные буферы.
Функция переносит все индексные дескрипторы, оставшиеся в списке
sb->s_io, обратно в список sb->s_dirty; они будут повторно рассмотрены
позже.
9. Уменьшает на единицу счетчик обращений индексного дескриптора.
10. Если wbc->nr_to_write больше 0, функция возвращается к шагу 2, чтобы
искать другие "грязные" индексные дескрипторы того же суперблока.
В противном случае syncsbinodes о завершает свою работу.
Далее продолжается выполнение функции writebackinodes(). Если wbc->
nrtowrite больше нуля, она переходит к обработке следующего суперблока
в глобальном списке. В противном случае заканчивает работу.
Запись старых грязных страниц на диск
Как было сказано ранее, ядро старается не допустить "голодной смерти" из-за
того, что некоторые страницы слишком долго не записываются на диск. По-
Поэтому, если какая-то страница остается грязной в течение заранее установ-
установленного отрезка времени, ядро принудительно запускает операцию ее записи
на диск.
Работа по нахождению старых грязных страниц поручается потоку ядра
pdflush, который периодически пробуждается. На этапе инициализации ядра
ФУНКЦИЯ pagewritebackinit () устанавливает ДИНамичеСКИЙ таймер wb_timer
так, ЧТОбы ОН ДОХОДИЛ ДО нулевого Значения через dirty_writeback_centisecs
сотых секунды (обычно 500, но это значение можно изменить в файле
/proc/sys/vm/dirty_writeback_centisecs). Функция-таймер wbtimerf n () вызы-
вызывает функцию pdf lushoperation (), передавая ей адрес функции обратного
ВЫЗОВа wb_kupdate ().
Функция wbkupdate () просматривает кэш страниц в поисках старых "гряз-
"грязных" индексных дескрипторов. Она выполняет следующие действия:
1. Вызывает функцию syncsupers () для записи "грязных" суперблоков на
диск. Хотя этот вызов, строго говоря, не имеет отношения к записи стра-
страниц, находящихся в кэше, он гарантирует, что никакой суперблок не оста-
останется "грязным" дольше, чем в течение пяти (как правило) секунд.
2. Записывает в поле olderthanthis дескриптора writebackcontroi указа-
указатель на значение в тиках, соответствующее текущему времени минус
30 секунд. Тридцать секунд— это максимальный интервал, в течение ко-
которого странице разрешается быть грязной.
3. На основании переменной pagestate (своей у каждого процессора) опре-
определяет приблизительное количество грязных страниц, находящихся в дан-
данный момент в кэше.
4. Многократно вызывает функцию writebackinodes о, пока либо количест-
количество страниц, записанных на диск, не достигнет значения, определенного на
предыдущем шаге, либо все страницы старше 30 секунд не будут записа-
записаны. В течение этого цикла выполнение функции может быть приостанов-
приостановлено, если очередь запросов переполнится.
5. Вызывает modtimer о для запуска динамического таймера wbtimer, кото-
который ДОЙдет ДО нуля через dirtywritebackcentisecs СОТЫХ секуНДЫ С МО-
мента вызова этой функции (или через одну секунду после настоящего
момента, если выполнение этой функции затянется надолго).
Системные вызовы
sync(), fsyncQ и fdatasyncf)
В этом разделе мы кратко опишем три системных вызова, доступных пользо-
пользовательским приложениям и позволяющих принудительно записать "грязные"
буферы на диск:
□ sync () — позволяет процессу записать на диск все "грязные" буферы;
□ f sync () — позволяет процессу записать на диск все блоки конкретного
открытого файла;
□ fdatasyncO — почти идентичен вызову f sync о, но не записывает блок
индексного дескриптора этого файла.
Системный вызов syncQ
Служебная процедура syssynco системного вызова sync о вызывает серию
дополнительных функций:
wakeup_bdflush @) ;
sync_inodes @) ;
sync_supers();
sync_filesystems @);
sync_filesystemsA);
sync_inodesA);
Как показано в предыдущем разделе, функция wakeupbdflush() запускает
поток ядра pdflush, который сбрасывает на диск все грязные страницы из кэ-
кэша страниц.
Функция syncinodes о сканирует список суперблоков в поисках "грязных"
индексных дескрипторов, которые следует сбросить на диск. Она принимает
параметр wait, который определяет, должна ли она подождать, пока запись на
диск будет выполнена, или нет. Функция сканирует суперблоки всех смонти-
смонтированных в этот момент файловых систем. Для каждого суперблока, содер-
содержащего "грязные" индексные дескрипторы, эта функция сначала вызывает
функцию syncsbinodes() для сброса на диск соответствующих грязных
страниц, а затем функцию syncbiockdev() — для явного сброса грязных
страниц буферов, владельцем которых является блочное устройство, содер-
содержащее этот суперблок. Это делается потому, что метод суперблока
writeinode во многих дисковых файловых системах просто помечает буфер
блока, соответствующий индексному дескриптору на диске, как "грязный".
Функция syncbiockdevo гарантирует, что изменения, внесенные функцией
syncsbinodes (), будут действительно записаны на диск.
Функция syncsupers () записывает на диск "грязные" суперблоки, если необ-
необходимо, используя операции суперблока writesuper. Наконец, функция
syncfiiesystemsO выполняет метод суперблока syncfs для всех файловых
систем, поддерживающих запись. Этот метод является вспомогательным ин-
инструментом, предлагаемым файловой системе на случай, если ей понадобятся
какие-то специальные операции при каждой синхронизации. Он используется
только в журналируемых файловых системах, например, в Ext3 (см. главу 18).
Обратите внимание, что функции syncinodes о и sync_fiiesystemsо вызы-
вызываются дважды, один раз с параметром wait, равным нулю, а второй — с этим
параметром, равным единице. Это сделано не случайно: вначале они быстро
сбрасывают на диск незаблокированные индексные дескрипторы, а затем
ждут разблокирования каждого незаблокированного индексного дескриптора
и записывают их на диск по одному.
Системные вызовы fsyncQ и fdatasyncQ
Системный вызов f sync о заставляет ядро записывать на диск все "грязные"
буферы, принадлежащие файлу, заданному с помощью параметра fd (вклю-
(включая и буфер, содержащий индексный дескриптор этого файла, если необхо-
необходимо). Соответствующая служебная процедура вычисляет адрес файлового
объекта и вызывает метод f sync. Как правило, этот метод сводится к вызову
функции writebacksingieinodeo, которая записывает как грязные стра-
страницы, ассоциированные с выбранным индексным дескриптором, так и сам
индексный дескриптор.
Системный вызов fdatasyncO весьма похож на fsync о, но записывает на
диск только буферы, содержащие данные файла, а не те, в которых хранится
информация индексного дескриптора. Поскольку в Linux 2.6 нет специфиче-
специфического файлового метода для fdatasync (), этот системный вызов использует
метод fsync и, следовательно, идентичен системному вызову fsync о.
ГЛАВА 16
Работа с файлами
Обращение к файлу на диске является довольно сложным видом деятельно-
деятельности, в которую вовлечены абстрактный слой виртуальной файловой системы
(см. главу 12), средства взаимодействия с блочными устройствами (см. гла-
главу 14) и кэш страниц (см. главу 15). В этой главе показано, как ядро выполня-
выполняет операции чтения и записи файлов, основываясь на упомянутых функцио-
функциональных возможностях. Материал этой главы относится как к обычным фай-
файлам, которые хранятся в дисковых файловых системах, так и к файлам
блочных устройств. Эти два типа файлов мы далее именуем просто файлами.
Этап работы, о котором идет речь в этой главе, начинается после вызова ме-
метода чтения или записи некоторого файла (как описано в главе 12). Мы пока-
покажем здесь, как каждая операция чтения заканчивается доставкой требуемых
данных процессу, работающему в режиме пользователя, а каждая операция
записи — данными, помеченными к отправке на диск. Собственно пересылка
данных выполняется средствами, описанными в главах 14 и 15.
Существует много разных способов обращения к файлу. В этой главе мы рас-
рассмотрим следующие случаи:
□ канонический режим — файл открывается при сброшенных флагах osync
и odirect, а доступ к его содержимому осуществляется системными вы-
вызовами read () и write (). В этом случае системный вызов read () блокирует
вызывающий процесс до тех пор, пока данные не будут скопированы в ад-
адресное пространство режима пользователя (впрочем, ядру разрешается
возвратить меньше байтов, чем было запрошено!). Системный вызов
write () работает совсем по-другому, поскольку он завершается, как толь-
только данные оказываются скопированы в кэш страниц (это называется "от-
"отложенной записью"). Этот случай описывается в разд. "Чтение и запись
файла'';
□ синхронный режим — файл открывается с установленным флагом osync,
либо флаг устанавливается позже системным вызовом f cnti (). Этот флаг
влияет только на операцию записи (операция чтения всегда блокирует
процесс), которая приостанавливает вызвавший процесс, пока данные не
будут фактически записаны на диск. Этот случай тоже рассмотрен в
разд. "Чтение и запись файла";
□ режим, отображения в память — после открытия файла приложение де-
делает системный вызов mmap (), чтобы отобразить файл в память. В резуль-
результате файл появляется в оперативной памяти в виде массива байтов, и при-
приложение обращается к элементам массива напрямую, без помощи систем-
системных вызовов reado, write о и iseek(). Этот случай обсуждается в
разд. "Отображение в память ";
□ режим прямого ввода/вывода — файл открывается с установленным фла-
флагом odirect. Любая операция чтения или записи переправляет данные не-
непосредственно из адресного пространства режима пользователя на диск
или в обратном направлении, в обход кэша страниц. Мы рассматриваем
этот случай в разд. "Прямой ввод/вывод" (значения флагов osync и
odirect могут быть скомбинированы четырьмя разными способами);
□ асинхронный режим — обращение к файлу происходит (либо через груп-
группу API-интерфейсов POSIX, либо при помощи системных вызовов, спе-
специфичных для Linux) таким образом, чтобы был выполнен "асинхронный
ввод/вывод". Это означает, что запросы на пересылку данных никогда не
блокируют вызывающий процесс. Вместо этого они выполняются в "фо-
"фоновом режиме", а приложение продолжает обычную работу. Этот случай
обсуждается в разд. "Асинхронный ввод/вывод".
Чтение и запись файла
В разд. "Системные вызовы readQ и writeQ " главы 12 описано, как реализо-
реализованы системные вызовы read () и write (). Соответствующие служебные про-
процедуры сводятся к выполнению методов read и write файловых объектов,
причем эти методы могут быть специфичными для файловой системы. У дис-
дисковых файловых систем эти методы находят физические блоки, содержащие
необходимые данные, и активизируют драйвер устройства, который запуска-
запускает пересылку данных.
Чтение файла происходит постранично; ядро всегда пересылает целые стра-
страницы данных за один прием. Если процесс делает системный вызов read (),
чтобы получить несколько байтов, а эти данные еще не находятся в опера-
оперативной памяти, ядро выделяет новый страничный кадр, заполняет его соот-
соответствующей порцией данных из файла, добавляет страницу в кэш страниц и
копирует запрошенные байты в адресное пространство процесса. В большин-
большинстве файловых систем чтение страницы данных из файла сводится к выясне-
выяснению, какой блок на диске содержит требуемые данные. Когда это сделано,
ядро заполняет страницы, выдавая необходимые команды ввода/вывода об-
общему слою работы с блочными устройствами. На практике метод read у всех
ДИСКОВЫХ фаЙЛОВЫХ СИСТем реализован общей функцией generic_file_read().
Операции записи в файлы на диске несколько сложнее, поскольку размер
файла может увеличиться, и, следовательно, ядро может выделить на диске
несколько физических блоков. Конечно, конкретная реализация этих дейст-
действий зависит от типа файловой системы. Тем не менее многие дисковые фай-
файловые системы реализуют свои методы write с помощью общей физической
функции, называемой genericfiiewrite о. Примерами таких файловых сис-
систем являются Ext2, System V/Coherent/Xenix и MINIX. С другой стороны, не-
некоторые файловые системы, например, журналируемые или сетевые, реали-
реализуют метод write специализированными функциями.
Чтение из файла
Функция generic_file_read() СЛуЖИТ ДЛЯ реализации метода read ДЛЯ обыч-
ных файлов и файлов блочных устройств почти во всех дисковых файловых
системах. Эта функция принимает следующие параметры:
□ f iip — адрес файлового объекта;
□ buf — линейный адрес области памяти режима пользователя, в которой
должны сохраняться символы, прочитанные из файла;
□ count — количество символов, которые следует прочитать;
П1 ppos — указатель на переменную, содержащую смещение, с которого
должно начаться чтение (как правило, это поле fpos файлового объекта
filp).
На первом шаге функция инициализирует два дескриптора. Первый хранится
в локальной переменной locaiiov типа iovec. Она содержит адрес (buf) и
длину (count) буфера режима пользователя, который будет принимать дан-
данные, прочитанные из файла. Второй дескриптор хранится в локальной пере-
переменной kiocb типа kiocb. Она используется для отслеживания завершения
текущей операции синхронного или асинхронного ввода/вывода. Основные
поля дескриптора kiocb перечислены в табл. 16.1.
ФуНКЦИЯ generic_file_read() инициализирует дескриптор kiocb, ВЫПОЛНЯЯ
макрос initsynckiocb, который устанавливает поля объекта для синхронной
операции. В частности, макрос устанавливает поле kikey в значение
kiocb sync key, поле kifiip — в значение filp, а поле kiobj — в значение
current.
Таблица 16.1. Основные поля дескриптора kiocb
Тип Поле Описание
struct list_head ki_run_list Указатели, используемые в списке
операций ввода/вывода, которые
необходимо повторить позднее
long ki_f lags Флаги дескриптора kiocb
int ki_users Счетчик обращений дескриптора
kiocb
unsigned int ki_key Идентификатор асинхронной опера-
операции ввода/вывода или значение
KIOCB_SYNC_KEY (Oxf f f f f f f f) ДЛЯ
синхронных операций ввода/вывода
struct file * ki_filp Указатель на файловый объект,
ассоциированный с текущей
операцией ввода/вывода
struct kioctx * ki_ctx Указатель на дескриптор контекста
асинхронного ввода/вывода для дан-
данной операции
int (*) (struct kiocb *, ki_cancel Метод, вызываемый при отмене опе-
struct io_event *) рации асинхронного ввода/вывода
ssize_t (*) (struct kiocb *) ki_retry Метод, вызываемый при попытке
повторного выполнения операции
асинхронного ввода/вывода
void (*) (struct kiocb *) ki_dtor Метод, вызываемый при уничтожении
дескриптора kiocb
struct list_head ki_list Указатели, используемые в списке
активных текущих операций вво-
ввода/вывода в асинхронном контексте
union ki_obj Для синхронных операций это указа-
указатель на дескриптор процесса, вы-
вызвавшего операцию ввода/вывода;
для асинхронных — указатель на
структуру iocb режима пользователя
u64 ki_user_data Значение, возвращаемое процессу,
работающему в режиме пользовате-
пользователя
lof f_t ki_pos Текущая позиция в файле для дан-
данной операции ввода/вывода
unsigned short ki_opcode Тип операции (чтение, запись или
синхронизация)
size_t ki_nbytes Количество байтов, подлежащих
передаче
Таблица 16.1 (окончание)
Тип Поле Описание
char * ki_buf Текущая позиция в буфере режима
пользователя
size_t ki_left Количество еще не переданных бай-
байтов
wait_queue_t ki_wait Очередь, используемая в асинхрон-
асинхронных операциях ввода/вывода
void * private Данные, используемые слоем фай-
файловых систем
Затем функция generic_file_read() вызывает функцию generic_file_aio_
read (), передавая ей адреса только что заполненных дескрипторов iovec и
kiocb. Последняя функция возвращает значение, как правило, показывающее
количество байтов, фактически прочитанных из файла. Функция generic_
f iieread () заканчивает работу, возвращая это значение.
Функция genericfiieaioreado представляет собой процедуру общего
назначения, используемую всеми файловыми системами для реализации как
синхронных, так и асинхронных операций чтения. Функция принимает четы-
четыре параметра: адрес iocb дескриптора kiocb, адрес iov массива дескрипторов
iovec, длину этого массива и адрес переменной ppos, в которой хранится те-
текущий указатель файла. Когда вызов делает функция genericfilereadO,
массив дескрипторов iovec состоит из единственного элемента, описывающе-
описывающего буфер режима пользователя, который будет принимать данные.
( Примечание )
Вариант системного вызова read(), называемый readv(), позволяет приложе-
приложению определить несколько буферов режима пользователя, по которым ядро
"разбрасывает" данные, прочитанные из файла, и функция generic_
file_aio_read() предусматривает такой случай. В последующем изложении
мы будем предполагать, что данные, прочитанные из файла, копируются в
единственный буфер режима пользователя. Догадаться о дополнительных
действиях, необходимых при использовании нескольких буферов, совсем не-
нетрудно.
Далее мы опишем действия, предпринимаемые функцией generic_fiie_
aioreado. Ради простоты, мы ограничимся самым общим случаем: син-
синхронная операция, выполняемая системным вызовом read о для файла с
кэшем страниц. Далее в этой главе мы разъясним поведение этой функции
в других случаях. Как обычно, мы не будем обсуждать обработку ошибок и
аномальных ситуаций.
Итак, функция выполняет следующие действия:
1. Вызывает accessoko, чтобы проверить корректность буфера режима
пользователя, описываемого дескриптором iovec. Поскольку адрес начала
и длина были получены от служебной процедуры sysreado, их нужно
проверять перед использованием (см. главу 10). Если параметры не
корректны, функция возвращает код ошибки -еfault.
2. Устанавливает дескриптор операции чтения. Это структура данных типа
readdescriptort, в которой хранится текущее состояние операции чте-
чтения файла, относящейся к единственному буферу режима пользователя.
Поля этого дескриптора перечислены в табл. 16.2.
3. Вызывает функцию dogenericfiiereado, передавая ей указатель на
файловый объект f iip, указатель на смещение ppos, адрес только что вы-
выделенного дескриптора операции чтения и адрес функции fiie_read_
actor ().
4. Возвращает количество байтов, скопированных в буфер режима пользова-
пользователя, Т. е. значение ПОЛЯ written структуры readdescriptort.
Таблица 16.2. Поля дескриптора операции чтения
Тип Поле Описание
size_t written Количество байтов, скопированных в буфер режима пользова-
пользователя
size_t count Количество еще не переданных байтов
char * arg.buf Текущая позиция в буфере режима пользователя
int error Код ошибки операции чтения @, если ошибок не было)
Функция dogenericfiiereado читает запрошенные страницы с диска и
копирует их в буфер режима пользователя. В частности, она выполняет сле-
следующие действия:
1. Получает объект addressspace, соответствующий читаемому файлу. Его
адрес находится В f ilp->f_mapping.
2. Получает владельца объекта addressspace, т. е. индексный дескриптор,
который будет владеть страницами, подлежащими заполнению данными
из файла. Его адрес хранится в поле host объекта addressspace. Если чи-
читаемый файл является файлом блочного устройства, владельцем является
индексный дескриптор специальной файловой системы bdev, а не индекс-
индексный Дескриптор, На КОТОРЫЙ указывает filp->f_dentry->d_inode (СМ.
разд. "Объект addressspace" главы 15).
3. Считает, что файл разбит на страницы (по 4096 байтов). По файловому
указателю *ppos функция вычисляет логический номер страницы, содер-
содержащей первый запрошенный байт— индекс страницы в адресном про-
пространстве и сохраняет его в локальной переменной index. Кроме того,
функция сохраняет в локальной переменной offset смещение первого за-
запрошенного байта внутри страницы.
4. Запускает цикл для чтения всех страниц, содержащих запрошенные бай-
байты. Количество байтов, которые надо прочитать, хранится в поле count
дескриптора read_descriptor_t.
5. За один проход цикла функция пересылает страницу данных, при этом,
если index*4096+offset превышает размер файла, хранящийся в поле
isize индексного дескриптора, функция выходит из цикла и переходит к
шагу 23.
6. Вызывает condreschedo, чтобы проверить флаг tifneedresched теку-
щего процесса и, если флаг установлен, вызвать функцию schedule ().
7. Если дополнительные страницы должны быть прочитаны заранее, функ-
функция ВЫЗЫВает ДЛЯ ЭТОГО функцию page_cache_readahead (). Мы ОТКЛадЫВа-
ем обсуждение опережающего чтения до разд. "Опережающее чтение
файлов".
8. Вызывает функцию f indgetpage (), передавая ей в качестве параметров
указатель на объект addressspace и значение index. Вызванная функция
просматривает кэш страниц в поисках дескриптора страницы, содержа-
содержащей запрошенные данные.
9. Если функция findgetpage() возвратила null, значит, запрошенной
страницы нет в кэше. В таком случае функция выполняет следующие
действия:
• вызывает handieramisso для подстройки параметров системы опе-
опережающего чтения;
• выделяет новую страницу;
• вставляет дескриптор новой страницы в кэш, для чего вызывает
addtopagecache (). Вспомним, что эта функция устанавливает у но-
новой страницы флаг PGiocked;
• вставляет дескриптор новой страницы в список LRU, вызвав
lru_cache_add() (см. главу 17);
• переходит к шагу 14, чтобы начать чтение данных из файла.
10. Если функция достигла этой точки, значит, страница находится в кэше.
Функция проверяет флаг PGuptodate. Если он установлен, данные, хра-
нящиеся в странице, свежие, и нет необходимости читать их с диска.
Функция переходит к шагу 17.
11. Данные на странице не имеют смысла и должны быть прочитаны с диска.
Функция получает исключительный доступ к странице, вызывая функ-
функцию lockpage (). Как сказано в разд. "Функции работы с кэшем стра-
страниц4 в главе 75, функция lockpage о приостанавливает текущий процесс,
если флаг PGiocked установлен, до тех пор, пока флаг не будет сброшен.
12. Итак, страница заблокирована текущим процессом. Однако не исключе-
исключено, что какой-то другой процесс удалил страницу из кэша непосредствен-
непосредственно перед предыдущим шагом. Поэтому функция проверяет, содержит ли
поле mapping дескриптора страницы значение null. В этом случае она раз-
разблокирует страницу, вызвав функцию uniockpage (), а затем уменьшит
счетчик обращений к странице (увеличенный функцией f indgetpage ())
и вернется к шагу 5, начав все заново с той же страницей.
13. Если функция дошла до этого шага, значит, страница заблокирована и все
еще находится в кэше. Функция снова проверяет флаг PGuptodate, пото-
потому что другой управляющий тракт ядра мог завершить необходимую
операцию чтения в момент между шагами 10 и 11. Если флаг установлен,
функция вызывает функцию uniockpage () и переходит к шагу 17, про-
пропуская операцию чтения.
14. В этот момент можно начать фактическую операцию ввода/вывода.
Функция вызывает метод readpage объекта addressspace данного файла.
Соответствующая функция отвечает за активизацию пересылки данных с
диска в страницу. Действия этой функции в отношении обычных файлов
и файлов блочных устройств обсуждаются чуть позже.
15. Если флаг PGuptodate все еще сброшен, функция ждет, пока страница не
будет фактически прочитана, вызывая функции lockpage(). Страница,
заблокированная на шаге 11, будет разблокирована сразу после оконча-
окончания операции чтения. Поэтому текущий процесс приостанавливается, по-
пока не завершится ввод/вывод.
16. Если значение index превышает размер файла в страницах (это число по-
получается в результате деления значения поля isize индексного дескрип-
дескриптора на 4096), функция уменьшает счетчик обращений страницы и выхо-
выходит из цикла, переходя на шаг 23. Этот случай имеет место, когда файл,
читаемый этим процессом, был усечен параллельным процессом.
17. Сохраняет в локальной переменной пг количество байтов страницы, под-
подлежащих копированию в буфер режима пользователя. Это значение рав-
равно размеру страницы D096 байтов), за исключением тех случаев, когда
значение offset не равно нулю (что возможно только для первой или по-
следней страницы запрошенных данных), либо файл содержит меньше
байтов, чем было запрошено.
18. Вызывает функцию mark_page_accessed(), чтобы установить флаг
PGreferenced или PGactive, отметив тот факт, что страница использует-
используется процессом, и ее нельзя выгружать (см. главу 17). Если одна и та же
страница (или ее часть) читается несколько раз в результате последова-
последовательных ВЫЗОВОВ функции do_generic_file_read(), ЭТОТ шаг выполняется
только при первом чтении.
19. В этот момент следует копировать данные из страницы в буфер режима
пользователя. С этой целью функция dogenericfiiereado вызывает
функцию fiiereadactoro, адрес которой был получен в качестве пара-
параметра. Со своей стороны, функция fiiereadactoro выполняет следую-
следующие действия:
• вызывает функцию kmap (), которая устанавливает постоянное отобра-
отображение страницы в адресное пространство ядра, если эта страница на-
находится в верхней памяти (см. главу 8);
• вызывает функцию copytouser(), которая копирует данные из
страницы в адресное пространство режима пользователя (см. главу 10).
Обратите внимание, что эта операция может блокировать процесс из-
за обработки ошибок типа "обращения к странице" в адресном
пространстве режима пользователя;
• вызывает функцию kunmapo для отмены постоянного отображения
страницы в адресное пространство ядра;
• обновляет ПОЛЯ count, written И buf Дескриптора readdescriptort.
20. Обновляет локальные переменные index и offset в соответствии с коли-
количеством байтов, фактически переданных в буфер режима пользователя.
Как правило, если последний байт страницы был скопирован в буфер
режима пользователя, переменная index увеличивается на единицу, а пе-
переменная offset приравнивается к нулю. В противном случае index не
увеличивается, а в offset записывается количество байтов, успешно ско-
скопированных из страницы в буфер режима пользователя.
21. Уменьшает счетчик обращений дескриптора страницы.
22. Если поле count дескриптора readdescriptort не равно нулю, значит,
нужно продолжить чтение данных из файла. Функция переходит к ша-
шагу 5, чтобы прочитать в цикле следующую страницу данных из файла.
23. Все запрошенные (или доступные) байты прочитаны. Функция обновляет
структуру данных опережающего чтения f iip->f_ra, чтобы отметить тот
факт, что данные последовательно читаются из файла (см. разд. "Опе-
"Опережающее чтение файлов" далее в этой главе).
24. Присваивает переменной *ppos значение index*4096+offset, отмечая сле-
следующую позицию для последовательного обращения на случай будущих
СИСТеМНЫХ ВЫЗОВОВ read () ИЛИ write () .
25. Вызывает функцию updateatime (), чтобы сохранить текущее время в по-
поле iatime индексного дескриптора файла и пометить индексный деск-
дескриптор как "грязный". После этого функция завершает работу.
Метод readpage для обычных файлов
Мы видели, что метод readpage неоднократно вызывается функцией
dogenericf ileread () ДЛЯ чтения ОТДеЛЬНЫХ СТраНИЦ ИЗ ДИСКа В память.
Метод readpage объекта addressspace содержит адрес функции, которая фак-
фактически активизирует пересылку данных с физического диска в кэш страниц.
Для обычных файлов это поле указывает на интерфейсную функцию, вызы-
вызывающую функцию mpagereadpage (). Например, в файловой системе Ext3 ме-
метод readpage реализован следующей функцией:
int ext3_readpage(struct file *file, struct page *page)
{
return mpage_readpage(page, ext3_get_block);
}
Интерфейсная функция необходима, ПОСКОЛЬКУ фуНКЦИЯ mpagereadpage ()
принимает в качестве параметров дескриптор page страницы, которую необ-
необходимо заполнить, и адрес getbiock функции, которая помогает функции
mpagereadpage () найти нужный блок. Интерфейсная функция специфична
для файловой системы и поэтому может предоставить необходимую функ-
функцию получения блока. Эта функция преобразует номера блоков относительно
начала файла в логические номера блоков в разделе диска (пример приводит-
приводится в главе 18). Естественно, последний параметр зависит от типа файловой
системы, которой принадлежит данный обычный файл. В предыдущем при-
примере параметром является адрес функции ext3_get_biock(). Функция, пере-
передаваемая как getbiock, всегда пользуется головой буфера для хранения важ-
важной информации о блочном устройстве (поле bdev), позиции запрошенных
данных в устройстве (поле bbiocknr) и состоянии блока (поле bstate).
Функция mpagereadpage () выбирает одну из двух стратегий чтения страницы
с диска. Если запрошенные данные содержатся в смежных блоках диска,
функция выдает команду на ввод/вывод общему блочному слою при помощи
одного дескриптора bio. В противном случае каждый блок страницы читается
с использованием отдельного дескриптора bio. Специфичная для файловой
системы функция getbiock играет решающую роль в определении, является
ли следующий блок в файле также и следующим блоком на диске.
ФуНКЦИЯ mpagereadpage () ВЫПОЛНЯет Следующие ДеЙСТВИЯ!
1. Проверяет поле PGprivate дескриптора страницы. Если оно установлено,
значит, это страница буферов, т. е. она связана со списком голов буферов,
описывающих блоки, составляющие страницу (см. разд. "Хранение блоков
в кэше страниц" главы 15). Это свидетельствует о том, что страница уже
была прочитана с диска в прошлом, а ее блоки не являются смежными на
диске. Функция переходит к шагу 11, чтобы выполнить поблочное чтение
страницы.
2. Извлекает размер блока (из поля page->mapping->host->i_bikbits индекс-
индексного дескриптора) и вычисляет два значения, необходимые для обращения
ко всем блокам этой страницы: количество блоков в странице и номер в
файле первого блока страницы, т. е. индекс первого блока страницы отно-
относительно начала файла.
3. Для каждого блока страницы функция вызывает специфичную для файло-
файловой системы функцию getbiock, полученную в качестве параметра, чтобы
узнать логический номер блока, т. е. индекс блока относительно начала
диска или раздела. Логические номера всех блоков в странице хранятся
в локальном массиве.
4. Проверяет, не возникли ли аномалии при выполнении предыдущего шага.
В частности, некоторые блоки могут оказаться не смежными на диске, или
какой-то блок может попасть в "дыру в файле" (см. разд. "Дыры в файлах"
главы 18), или буфер блока уже мог быть заполнен функцией getbiock.
Во всех этих случаях описываемая функция переходит к шагу 11, чтобы
выполнить поблочное чтение страницы.
5. Если функция дошла до этого шага, значит, все блоки в странице являются
смежными на диске. Однако страница могла быть последней страницей
данных в файле, и поэтому некоторые из ее блоков, возможно, не имеют
образа на диске. Если это действительно так, функция заполняет нулями
соответствующие буферы блоков на странице. В противном случае она ус-
устанавливает флаг PGmappedtodisk дескриптора страницы.
6. Вызывает функцию bioaiiocO для выделения нового дескриптора bio,
состоящего из единственного сегмента, и для инициализации полей
bibdev и bisector этого дескриптора адресом дескриптора блочного уст-
устройства и логическим номером первого блока страницы соответственно.
Оба информационных элемента были определены в шаге 3.
7. Записывает в дескриптор biovec сегмента, принадлежащего bio, началь-
начальный адрес страницы, смещение первого байта, подлежащего чтению (то
есть ноль), и общее количество байтов, которые необходимо прочитать.
8. Сохраняет адрес функции mpage_end_io_read () В ПОЛе bio->bi_end_io.
9. Вызывает функцию submitbio (), которая устанавливает флаг birw в со-
соответствии с направлением движения данных, обновляет переменную
pagestates (свою у каждого процессора), чтобы отследить количество
ПрОЧИТаННЫХ СеКТОрОВ, И, Наконец, ВЫЗЫВаеТ фуНКЦИЮ generic_make_
request о для дескриптора bio (см. разд. "Выдача запроса планировщику
ввода/вывода" главы 14).
10. Возвращает ноль (успех).
11. Если функция находится на этом шаге, значит, страница содержит блоки,
не являющиеся смежными на диске. Если страница не устарела (флаг
PGuptodate установлен), фуНКЦИЯ ВЫЗЫВаеТ фуНКЦИЮ unlock_page () ДЛЯ
разблокировки страницы; в противном случае она вызывает функцию
biockreadf uiipage (), чтобы начать поблочное чтение страницы.
12. Возвращает ноль (успех).
Функция mpageendioread () является методом завершения для дескриптора
bio. Она выполняется сразу по окончании пересылки данных. Если предпо-
предположить, что ошибок ввода/вывода не было, функция устанавливает флаг
PGuptodate дескриптора страницы, вызывает функцию unlockpage (), чтобы
разблокировать страницу и возобновить выполнение процессов, возможно,
ожидающих это событие, и вызывает функцию bioput (), чтобы уничтожить
дескриптор bio.
Метод readpage для файлов блочных устройств
В разд. "Работа с файлами устройств в VFS" главы 13 и разд. "Открытие
фата блочного устройства" главы 14 мы обсуждали, как ядро обрабатывает
запросы на открытие файла блочного устройства. Мы видели, как функция
initspeciaiinode () устанавливает индексный дескриптор устройства и как
функция bikdevopen () завершает этап открытия файла.
Блочные устройства используют объект addressspace, который хранится в
поле idata соответствующего индексного дескриптора в специальной фай-
файловой системе bdev. В отличие от обычных файлов, у которых метод readpage
в объекте addressspace зависит от типа файловой системы, которой принад-
принадлежит файл, метод readpage файлов блочных устройств всегда один и тот же.
Он реализован функцией bikdevreadpageo, которая вызывает функцию
block_read_full_page() I
int blkdev_readpage(struct file * file, struct * page page)
{
return block_read_full_page(page, blkdev_get_block);
}
Как видите, здесь опять интерфейсная функция, на этот раз для
biockreadfuiipageo. Ее второй параметр указывает на функцию, которая
преобразует номер блока в файле в логический номер блока относительно
начала блочного устройства. У файлов блочных устройств эти два номера
совпадают, и функция bikdevgetbiock () выполняет следующие действия:
1. Проверяет, превышает ли номер первого блока в странице индекс послед-
последнего блока в блочном устройстве (этот индекс получается делением раз-
размера блОЧНОГО устройства, КОТОрЫЙ ХраНИТСЯ В ПОЛе bdev->bd_inode->
isize, на размер блока, хранящийся в поле bdev->bd_biock_size, причем
bdev указывает на дескриптор блочного устройства). Если проверка дала
положительный результат, функция возвращает -ею для операции записи
или ноль для операции чтения. (Чтение после конца блочного устройства
тоже не разрешено, но здесь не следует возвращать код ошибки: ядро,
возможно, просто пыталось передать запрос на чтение последней порции
данных блочного устройства, а соответствующая страница буферов ото-
отображена не полностью.)
2. Записывает в поле bdev головы буфера значение bdev.
3. Записывает в поле bbiocknr головы буфера номер блока в файле, пере-
переданный в качестве параметра.
4. Устанавливает флаг BHMapped головы буфера, чтобы обозначить осмыс-
осмысленность значений полей bdev и bbiocknr головы буфера.
Функция biockreadfuiipage о читает страницу данных по блокам. Как мы
уже видели, она вызывается при чтении файлов блочных устройств и при
чтении страниц обычных файлов, блоки которых не являются смежными на
диске. Она выполняет следующие действия:
1. Проверяет флаг PGprivate дескриптора страницы. Если он установлен,
значит, страница ассоциирована со списком голов буферов, описывающих
блоки, составляющие страницу (см. разд. "Хранение блоков в кэше стра-
страниц" главы 15). В противном случае функция вызывает функцию
createemptybuffers о для выделения голов всем буферам блоков, вклю-
включенным в страницу. Адрес головы первого буфера в странице хранится в
поле page->private. Поле bthispage каждой головы буфера указывает на
голову следующего буфера в странице.
2. По смещению в файле относительно страницы (поле page->index) вычис-
вычисляет номер в файле для первого блока на странице.
3. Для каждой головы буфера на странице выполняет следующие действия:
• если установлен флаг BHUptodate, функция пропускает буфер и пере-
переходит к следующему буферу на странице;
• если флаг BHMapped не установлен, а блок не находится после конца
файла, функция вызывает специфичную для файловой системы функ-
функцию getbiock, адрес которой был получен в качестве параметра. Для
обычного файла функция выполняет поиск в дисковых структурах фай-
файловой системы и находит логический номер блока буфера относительно
начала диска или раздела. Что касается файла блочного устройства,
функция считает номер блока в файле логическим номером блока.
В обоих случаях функция сохраняет логический номер блока в поле
bbiocknr соответствующей головы буфера и устанавливает флаг
BH_Mapped1;
• снова проверяет флаг BHUptodate, потому что специфичная для файло-
файловой системы функция getbiock могла запустить операцию блочного
ввода/вывода, которая обновила буфер. Если флаг BHUptodate установ-
установлен, функция переходит к следующему буферу в странице;
• сохраняет адрес головы буфера в локальном массиве агг и переходит к
следующему буферу на странице.
4. Если на предыдущем шаге не была обнаружена "дыра" в файле, функция
устанавливает для страницы флаг PGmappedtodisk.
5. В настоящий момент локальный массив агг содержит адреса голов тех
буферов, содержимое которых устарело. Если тот массив пуст, значит, все
буферы страницы содержат осмысленные данные. Поэтому функция уста-
устанавливает флаг PGuptodate дескриптора этой страницы, снимает с нее
блокировку при помощи функции uniockpage () и возвращает управление.
6. Локальный массив агг не пуст. Для каждой головы буфера из массива
функция biockreadf uiipage () выполняет следующие действия:
• устанавливает флаг внъоск. Если он уже установлен, ждет, пока осво-
освободится буфер;
• записывает в поле bendio головы буфера адрес функции
end_buffer_async_read()H устанавливает флаг BH_Async_Read ГОЛОВЫ
буфера.
7. Для каждой головы буфера из локального массива агг вызывает функцию
submitbh (), указывая read в качестве типа операции. Как мы видели ра-
ранее, эта функция запускает ввод/вывод соответствующего блока.
8. Возвращает 0.
1 При обращении к обычному файлу функция get_block может не найти блок, если он попадает в
"дыру в файле". В таком случае функция заполняет буфер блока нулями и устанавливает флаг
BHUptodate головы буфера.
Функция endbufferasyncreado является методом завершения для головы
буфера; она выполняется сразу после окончания пересылки данных для бу-
буфера блока. Если предположить, что ошибок ввода/вывода не было, функция
устанавливает флаг BHUptodate головы буфера и сбрасывает флаг
BHAsyncRead. Затем функция получает дескриптор страницы буферов, со-
содержащей буфер блока (адрес хранится в поле bpage головы буфера), и про-
проверяет, нет ли устаревших блоков на странице. Если ни один не устарел,
функция устанавливает для страницы флаг PGuptodate и вызывает функцию
unlock_page().
Опережающее чтение файлов
Многие обращения к диску являются последовательными. Как мы увидим в
главе 18, обычные файлы хранятся на диске в виде больших групп смежных
секторов, так что их данные могут быть быстро получены несколькими дви-
движениями головок. Когда программа читает или копирует файл, она нередко
обращается к нему последовательно, с первого байта до последнего. Поэтому
весьма вероятно, что много смежных секторов на диске будут прочитаны при
обработке серии запросов процесса к одному файлу.
Опережающее чтение заключается в чтении нескольких смежных страниц
данных из обычного файла или файла блочного устройства до того, как они
будут запрошены фактически. В большинстве случаев опережающее чтение
значительно повышает производительность, поскольку позволяет контролле-
контроллеру диска обрабатывать меньше команд, каждая из которых затрагивает боль-
большие участки из смежных секторов. Кроме того, опережающее чтение улуч-
улучшает время отклика системы. Процесс, последовательно читающий файл,
обычно не должен ждать запрошенные данные, потому что они уже находят-
находятся в оперативной памяти.
Однако в опережающем чтении нет смысла, если приложение обращается к
файлам произвольным образом. В этом случае опережающее чтение фактиче-
фактически наносит вред, поскольку засоряет кэш страниц ненужной информацией.
Поэтому ядро сокращает или прекращает опережающее чтение, когда опре-
определяет, что последний запрос на ввод/вывод не является последовательным
по отношению к предыдущему.
Для опережающего чтения файлов требуется сложный алгоритм по несколь-
нескольким причинам:
П поскольку данные читаются постранично, алгоритм опережающего чтения
не должен принимать во внимание смещение внутри страницы; его инте-
интересуют только позиции страниц внутри файла;
□ опережающее чтение может постепенно становиться все более интенсив-
интенсивным, по мере того, как процесс продолжает последовательно обращаться к
файлу;
□ опережающее чтение должно быть сокращено или вовсе отключено, если
запрос от текущего процесса не является последовательным по отноше-
отношению к предыдущему (случай произвольного доступа);
□ опережающее чтение должно быть прекращено, когда процесс снова и
снова обращается к одним и тем же страницам (используется только не-
небольшая порция файла), или когда почти все страницы файла находятся
в кэше страниц;
□ драйвер ввода/вывода низкого уровня должен активизироваться вовремя,
чтобы следующие страницы уже были получены к моменту, когда они по-
понадобятся процессу.
Ядро считает обращение к файлу последовательным по отношению к преды-
предыдущему, если первая запрошенная страница непосредственно следует за по-
последней запрошенной страницей в предыдущем обращении.
При обращении к данному файлу алгоритм опережающего чтения пользуется
двумя наборами страниц, каждый из которых соответствует непрерывному
участку файла. Эти два набора называются текущим окном и опережающим
окном.
Текущее окно состоит из страниц, запрошенных процессом или заранее про-
прочитанных ядром и включенных в кэш страниц. (Страница в текущем окне
может быть устаревшей, если ввод/вывод данных еще не закончился.) Теку-
Текущее окно может содержать как страницы недавно и последовательно запро-
запрошенные процессом, так и некоторые из страниц, прочитанных ядром заранее,
но еще не затребованных процессом.
Опережающее окно состоит из страниц (следующих за страницами текущего
окна), которые в настоящий момент читаются ядром "впрок". Ни одна из
страниц в этом окне еще не запрошена процессом, но ядро предполагает, что
рано или поздно они процессу потребуются.
Когда ядро распознает последовательное обращение, а первая страница нахо-
находится в текущем окне, ядро проверяет, существует ли опережающее окно.
Если еще нет, ядро создает новое опережающее окно и запускает операцию
чтения соответствующих страниц. В идеальном случае процесс запрашивает
страницы из текущего окна, в то время как другие страницы передаются в
опережающее окно. Когда процесс запрашивает страницу из опережающего
окна, оно становится новым текущим окном.
Основная структура данных, используемая алгоритмом опережающего чте-
чтения,— это дескриптор filerastate, поля которого перечислены в
табл. 16.3. У каждого файлового объекта есть такой дескриптор в поле fra.
Таблица 16.3. Поля дескриптора file_ra_state
Тип Поле Описание
unsigned long start Индекс первой страницы в текущем окне
unsigned long size Количество страниц в текущем окне (-1 означает,
что опережающее чтение временно отключено,
О — текущее окно пусто)
unsigned long flags Флаги, применяемые для управления опережающим
чтением
unsigned long cache_hit Количество последовательных попаданий в кэш (число
страниц, запрошенных процессом и найденных в кэше)
unsigned long prev_page Индекс последней страницы, запрошенной процессом
unsigned long aheadstart Индекс первой страницы в опережающем окне
unsigned long ahead_size Количество страниц в опережающем окне @ означает,
что окно пусто)
unsigned long ra_pages Максимальный размер окна опережающего чтения
в страницах @ означает, что опережающее чтение
отключено на постоянной основе)
unsigned long mmap_hit Счетчик попаданий опережающего чтения
(используется для файлов, отображенных в память)
unsigned long mmap_miss Счетчик промахов опережающего чтения (используется
для файлов, отображенных в память)
Когда файл открывается, все поля соответствующего дескриптора
filerastate приравнены К нулю, За исключением полей prev_page И
ra_pages.
В поле prevpage хранится индекс последней страницы, запрошенной процес-
процессом в предыдущей операции чтения; изначально это поле содержит -1.
Поле rapages представляет максимальный размер текущего окна в страни-
страницах, т. е. устанавливает предел опережающего чтения для данного файла.
Значение по умолчанию для этого поля хранится в дескрипторе
backingdevinfo блочного устройства, на котором хранится файл (см.
разд. "Дескрипторы очереди запросов" главы 14). Приложение может на-
настроить алгоритм опережающего чтения для конкретного открытого файла,
модифицируя поле rapages. Для этого оно должно сделать системный вызов
posix_fadvise о, передав ему команды posix_fadv_normal (установить макси-
максимальный размер опережающего чтения по умолчанию, обычно 32 страницы),
posixfadvsequential (установить максимальный размер опережающего чте-
чтения в два раза больше значения по умолчанию) или posixfadvrandom (уста-
новить максимальный размер опережающего чтения равным нулю, тем са-
самым отключив опережающее чтение).
Поле flags содержит два флага, ra_flag_miss и ra_flag_incache, играющих
важную роль в опережающем чтении. Первый флаг устанавливается, если
страница, прочитанная заранее, не найдена в кэше (вероятнее всего, из-за
утилизации ее ядром в целях освобождения памяти; см. главу 17). В этом слу-
случае размер следующего создаваемого опережающего окна будет несколько
уменьшен. Второй флаг устанавливается, когда ядро выясняет, что последние
256 страниц, запрошенных процессом, все были найдены в кэше страниц (ко-
(количество последовательных успешных обращений к кэшу хранится в поле
ra->cache_hit). В этом случае опережающее чтение отключается, поскольку
ядро предполагает, что все страницы, затребованные процессом, уже нахо-
находятся в кэше.
Когда выполняется алгоритм опережающего чтения? В следующих случаях:
□ когда ядро-обрабатывает запрос на чтение страниц из файла от процесса,
работающего в режиме пользователя; это событие приводит к вызову
функции page_cache_readahead ();
□ когда ядро выделяет страницу для отображения файла в память (функция
filemapnopageO в разд. "Выделение страниц по требованию для ото-
бражения в память" далее в этой главе тоже вызывает функцию
page_cache_readahead ());
□ когда приложение, работающее в режиме пользователя, выполняет сис-
системный вызов readahead (), который явным образом запускает опережаю-
опережающее чтение по дескриптору файла;
□ когда приложение, работающее в режиме пользователя, выполняет сис-
системный ВЫЗОВ posix_f advise () С Командами POSIX_FADV_NOREUSE ИЛИ
posixfadvwillneed, которые информируют ядро, что указанный диапазон
страниц файла будет запрошен в ближайшем будущем;
□ когда приложение, работающее в режиме пользователя, выполняет сис-
системный ВЫЗОВ madviseO С КОМаНДОЙ MADVWILLNEED, КОТОраЯ Информирует
ядро, что указанный диапазон страниц в области отображения файла в па-
память будет запрошен в ближайшем будущем.
Функция page_cache_readahead()
Функция pagecachereadahead () берет на себя ответственность за все опера-
операции опережающего чтения, которые не были явно запущены соответствую-
соответствующими системными вызовами. Она пополняет текущее и опережающее окна,
изменяет их размеры в соответствии с количеством "попаданий", т. е. соглас-
но тому, насколько успешной была стратегия опережающего чтения при пре-
предыдущих обращениях к файлу.
Функция вызывается, когда ядро должно удовлетворить запрос на чтение од-
одной или нескольких страниц файла. Она принимает пять параметров:
П mapping— указатель на объект addressspace, который описывает вла-
владельца страницы;
П га — указатель на дескриптор f ilerastate файла, содержащего стра-
страницу;
□ f iip — адрес файлового объекта;
□ offset — смещение страницы внутри файла;
□ reqsize — количество страниц, которые необходимо прочесть, чтобы за-
завершить текущую операцию чтения.
( Примечание ^
На самом деле, если операция чтения включает в себя количество страниц,
превышающее максимальный размер окна опережающего чтения, функция
pagecachereadahead () вызывается несколько раз. Таким образом, параметр
reqsize может быть меньше количества страниц, которые необходимо про-
прочесть, чтобы завершить текущую операцию чтения.
На рис. 16.1 приведена блОК-СХема функции pagecachereadahead (). Функция
воздействует на поле дескриптора f ilerastate. Таким образом, хотя описа-
описание действий в блок-схеме весьма неформально, вы легко догадаетесь, какие
шаги выполняет функция. Например, чтобы проверить, совпадает ли запро-
запрошенная страница с предыдущей прочитанной страницей, функция проверяет,
совпадают ли значения поля ra->prev_page и параметра offset (см. табл. 16.3).
Когда процесс впервые обращается к файлу, и первая запрошенная страница
имеет в файле нулевое смещение, функция предполагает, что процесс будет
выполнять последовательные обращения. Поэтому она создает новое текущее
окно, начиная с первой страницы. Длина первого текущего окна всегда явля-
является степенью двойки и некоторым образом зависит от количества страниц,
запрошенных процессом при первой операции чтения: чем больше страниц
запрошено, тем больше текущее окно, вплоть до максимума, хранящегося в
поле ra->ra_pages. И наоборот, если процесс впервые обращается к файлу, но
смещение первой запрошенной страницы не равно нулю, функция предпола-
предполагает, что процесс не будет выполнять последовательные обращения. Тогда
она временно отключает опережающее чтение (устанавливает поле ra->size в
значение-1). Однако новое текущее окно создается, когда функция распо-
распознает последовательное обращение при временно отключенном опережаю-
опережающем чтении.
Рис. 16.1. Блок-схема работы функции page_cache_readahead ()
Если опережающее окно еще не существует, оно создается, как только функ-
функция обнаружит, что процесс выполнил последовательное обращение к теку-
текущему окну. Опережающее окно всегда начинается со страницы, следующей
за последней страницей текущего окна. Однако его длина зависит от длины
текущего окна: если флаг raflagmiss установлен, длина опережающего ок-
окна равна длине текущего окна минус 2, или четырем страницам, если резуль-
результат меньше 4; в противном случае длина опережающего окна либо вчетверо,
либо вдвое больше длины текущего. Если процесс продолжает обращаться к
файлу последовательно, опережающее окно, в конце концов, становится но-
новым текущим окном, и создается новое опережающее окно. Таким образом,
опережающее чтение агрессивно интенсифицируется, если процесс читает
файл последовательно.
Как только функция распознает, что обращение к файлу не является последо-
последовательным по отношению к предыдущему, текущее и опережающее окна
очищаются, а опережающее чтение временно отключается. Оно возобновля-
возобновляется "с нуля", как только процесс выполнит операцию чтения, последова-
последовательную по отношению к предыдущему обращению к файлу.
ВСЯКИЙ раз, КОГДа фунКЦИЯ page_cache_readahead() СОЗДает НОВОе ОКНО, Она
запускает операцию чтения для страниц, входящих в окно. Чтобы прочитать
Группу Страниц, фуНКЦИЯ pagecachereadahead () ВЫЗЫВает фуНКЦИЮ
blockablepagecachereadahead (). Для уменьшения нагрузки на ядро ПОСЛеД-
няя функция обладает следующими интеллектуальными возможностями:
□ чтение не выполняется, если очередь запросов, обслуживающая данное
блочное устройство, переполнена запросами на чтение (нет смысла пере-
переполнять ее дальше и блокировать операцию опережающего чтения);
□ для каждой страницы, подлежащей чтению, проверяется ее наличие в
кэше страниц. Если страница уже присутствует в кэше, функция пропус-
пропускает ее;
□ все страничные кадры, необходимые запросу на чтение, выделяются сразу,
до выполнения чтения с диска. Если не все страничные кадры могут быть
получены, опережающее чтение выполняется только для доступных стра-
страниц. Нет никакого смысла откладывать опережающее чтение до получе-
получения всех страничных кадров;
П когда это возможно, запросы на операции чтения передаются общему
слою работы с блочными устройствами при помощи многосегментных де-
дескрипторов bio (см. разд. "Сегменты" главы 14). Это делается специали-
специализированным методом readpages объекта addressspace, если ЭТОТ метод
определен. Если же он не определен, вызывается метод readpage. Послед-
Последний был описан в разд. "Чтение из файла" ранее в этой главе для случая
одного сегмента, но читатель легко расширит этот описание и для случая с
несколькими сегментами.
Функция handle__ra_miss()
В некоторых случаях ядро должно корректировать параметры опережающего
чтения, если стратегия оказывается не очень эффективной. Рассмотрим
функцию do_generic_fiie_read(), описанную в разд. "Чтение из файла"ра-
файла"ранее в этой главе. На шаге 7 вызывается функция page_cache_readahead().
Блок-схема на рис. 16.1 выделяет два случая: либо запрошенная страница на-
находится в текущем или опережающем окне и, вероятнее всего, была прочита-
прочитана заранее, либо это не так, и для ее чтения вызывается функция
blockable_page_cache_readahead (). В обоих случаях функция do_generic_
file read о должна найти страницу в кэше на шаге 8. Если ей это не удается,
значит, алгоритм утилизации страничных кадров удалил эту страницу из кэ-
кэша. Тогда функция do_generic_file_read() вызывает фунКЦИЮ
handieramiss о, которая настраивает алгоритм опережающего чтения, уста-
устанавливая флаг ra_flag_miss и сбрасывая флаг ra_flag_incache.
Запись в файл
Вспомним, что системный вызов write (), среди прочего, перемещает данные
из адресного пространства режима пользователя вызывающего процесса в
структуры ядра, а затем — на диск. Метод write файлового объекта разреша-
разрешает файловой системе любого типа определять специализированную операцию
записи. В системе Linux 2.6 метод write каждой дисковой файловой системы
является процедурой, которая идентифицирует блоки диска, вовлеченные в
операцию записи, копирует данные из адресного пространства режима поль-
пользователя в страницы, принадлежащие кэшу, и помечает буферы в этих стра-
страницах как "грязные".
Многие файловые системы (в том числе Ext2 и JFS) реализуют метод write
файлового объекта с помощью функции genericfiiewriteo, которая при-
принимает следующие параметры:
□ file — указатель на файловый объект;
□ buf — адрес в адресном пространстве режима пользователя, по которому
находятся символы, подлежащие записи в файл;
□ count — количество символов, которые должны быть записаны в файл;
□ ppos — адрес переменной, содержащей смещение внутри файла, с которо-
которого должна начаться запись.
Функция выполняет следующие шаги:
1. Инициализирует локальную переменную типа iovec, содержащую адрес и
длину буфера режима пользователя.
2. Определяет адрес inode индексного дескриптора, соответствующего фай-
файлу, в который пишутся данные (fiie->f_mapping->host), и получает сема-
семафор inode->i_sem. Благодаря этому семафору только один процесс сможет
выполнить системный вызов write () для файла.
3. Вызывает макрос initsynckiocb, чтобы проинициализировать локаль-
локальную переменную типа kiocb. Как было сказано ранее в этой главе, данный
макрос записывает в поле kikey значение kiocbsynckey (операция син-
синхронного ввода/вывода), в поле kifilp— значение file, а в поле
ki_obj — значение current.
4. Вызывает generic_file_aio_write_nolock(), чтобы пометить затронутые
страницы как "грязные". В качестве параметров передает вызванной
функции адреса локальных переменных типа iovec и kiocb, количество
сегментов для буфера режима пользователя (в этом случае — только один)
и параметр ppos.
5. Освобождает Семафор inode->i_sem.
6. Проверяет флаг osync файла, флаг ssync индексного дескриптора и флаг
mssynchronous суперблока. Если хотя бы один из них установлен, функ-
функция вызывает функцию syncj?age_range(), заставляя ядро сбросить на
диск все страницы из кэша, которые были затронуты на шаге 4. При этом
текущий процесс блокируется до окончания операции ввода/вывода. Со
своей стороны, функция syncpagerange() выполняет либо метод
writepages объекта addressspace, если этот метод определен, либо функ-
функцию mpagewritepages () (см. разд. "Запись грязных страниц на диск" далее
в этой главе), чтобы запустить операцию записи для "грязных" страниц;
затем она вызывает функцию generic_osync_inode(), чтобы сбросить на
диск индексный дескриптор и ассоциированные буферы, и, наконец, вы-
вызывает функцию waitonpagebit (), чтобы приостановить текущий про-
процесс до тех пор, пока все биты PGwriteback страниц, записываемых на
диск, не будут сброшены.
7. Возвращает КОД ВОЗВрата, ПОЛучеННЫЙ ОТ фуНКЦИИ generic_file_aio_
writenoiock(). Как правило, это количество фактически записанных
байтов.
Функция genericfiieaiowritenoiocko принимает четыре параметра:
адрес iocb дескриптора kiocb, адрес iov массива дескрипторов iovec, длину
этого массива и адрес переменной ppos, в которой хранится указатель файла.
Если функция была вызвана функцией generic_f iie_write (), то массив деск-
дескрипторов iovec состоит из одного элемента, описывающего буфер режима
пользователя, содержащий данные, подлежащие записи в файл.
( Примечание ~^
Вариант системного вызова write (), называемый writev (), позволяет прило-
приложению определить несколько буферов режима пользователя, из которых ядро
получает данные, подлежащие записи в файл. Функция generic_file_
aiowritenolock () обрабатывает и этот случай. В дальнейшем мы будем
предполагать, что данные получены из одного буфера режима пользователя,
но читатель без труда догадается, какие дополнительные действия необходи-
необходимы для работы с несколькими буферами.
Сейчас МЫ ПОЯСНИМ работу функции generic_file_aio_write_nolock(). Ради
простоты, мы ограничимся самым общим случаем: операция в обычном ре-
режиме была запущена системным вызовом write о для кэшируемого файла.
Далее в этой главе мы опишем, как функция ведет себя в других случаях. Как
всегда, мы не будем обсуждать обработку ошибок и аномальных ситуаций.
Функция выполняет следующие действия:
1. Вызывает функцию accessoko, чтобы проверить корректность буфера
режима пользователя, описанного дескриптором iovec (начальный адрес и
длина были получены от служебной процедуры syswrite (), поэтому их
надо проверить перед использованием; см. главу 10). Если параметры не-
недопустимы, функция возвращает код ошибки -еfault.
2. Определяет адрес inode индексного дескриптора, соответствующего фай-
файлу, в который производится запись (fiie->f_mapping->host). Вспомним,
что если это файл блочного устройства, то индексный дескриптор принад-
принадлежит специальной файловой системе (см. главу 14).
3. Записывает в поле current->backing_dev_info адрес дескриптора
backing_dev_info ЭТОГО файла (file->f_mapping->backing_dev_info). Фак-
тически, это присваивание позволит текущему процессу записывать на
диск "грязные" страницы, которыми владеет fiie->f_mapping, даже если
соответствующая очередь запросов переполнена (см. главу 17).
4. Если флаг oappend в поле fiie->fiags установлен, а файл обычный (не
файл блочного устройства), то функция устанавливает указатель *ppos на
конец файла, чтобы новые данные добавлялись к нему.
5. Выполняет несколько проверок размера файла. Например, операция запи-
записи не должна увеличивать обычный файл настолько, что он превысит ли-
лимит для данного пользователя, хранящийся в элементе current->signai->
riiin[RLiMiT_FSizE] (см. главу 3), и лимит для файловой системы, храня-
хранящийся в поле inode->i_sb->s_maxbytes. Кроме того, если файл не является
"большим файлом" (флаг olargefile в поле f iie->f_f lags сброшен), его
размер не может превышать 2 Гбайт. Если любое из этих ограничений на-
нарушено, функция уменьшает количество байтов, подлежащих записи.
6. Если флаг suid файла установлен, функция сбрасывает его. Она также
сбрасывает флаг sgid, если файл исполняемый (см. главу 1). Мы ведь не
хотим, чтобы пользователи могли модифицировать файлы setuid?
7. Сохраняет текущее время суток в поле inode->mtime (это время последней
операции записи в файл) и в поле inode->ctime (время последнего измене-
изменения индексного дескриптора) и помечает индексный дескриптор как
"грязный".
8. Запускает цикл для обновления всех страниц файла, вовлеченных в опера-
операцию записи. На каждом шаге цикла функция выполняет следующие дей-
действия:
• вызывает функцию f indiockpage () для поиска страницы в кэше (см.
разд. ''Функции работы с кэшем страниц" главы 15). Если вызванная
функция находит страницу, она увеличивает ее счетчик обращений и
устанавливает флаг PGiocked;
• если страницы нет в кэше, функция выделяет новый страничный кадр и
вызывает функцию addtopagecache (), чтобы занести страницу в кэш.
Как было сказано в главе 75, эта функция также увеличивает счетчик
обращений и устанавливает флаг PGiocked. Кроме того, функция зано-
заносит новую страницу в список неактивных страниц в зоне памяти (см.
главу 17);
• вызывает метод preparewrite объекта addressspace индексного деск-
дескриптора (fiie->f_mapping). Соответствующая функция отвечает за вы-
выделение и инициализацию голов буферов для страницы. В последую-
последующих разделах мы обсудим, как эта функция работает с обычными фай-
файлами и файлами блочных устройств;
• если буфер находится в верхней памяти, функция задает отображение
буфера режима пользователя в адресное пространство ядра (см. гла-
главу 8). Затем она вызывает функцию copyf romuser (), чтобы скопиро-
скопировать символы из буфера режима пользователя в страницу, а затем от-
отменяет отображение в адресное пространство ядра;
• вызывает метод commitwrite объекта addressspace индексного деск-
дескриптора (fiie->f_mapping). Функция, реализующая метод, помечает со-
соответствующие буферы как "грязные", чтобы они впоследствии были
записаны на диск. В следующих двух разделах мы обсудим, как эта
функция работает с обычными файлами и файлами блочных устройств;
• вызывает фуНКЦИЮ unlock_page(), Чтобы сбрОСИТЬ флаг PGlocked И
возобновить выполнение процессов, ждущих эту страницу, если тако-
таковые имеются;
• вызывает фуНКЦИЮ mark_page_accessed(), чтобы обновить информа-
цию о состоянии страницы для алгоритма утилизации памяти (см.
разд. "Списки давно неиспользуемых страниц (LRU) " главы 17);
• уменьшает счетчик обращений к странице, чтобы компенсировать его
увеличение в начале цикла;
• на этом шаге цикла "загрязнена" еще одна страница, и функция прове-
проверяет, превысила ли доля "грязных" страниц в кэше некоторый фикси-
фиксированный порог (обычно составляющий 40% от количества страниц в
системе). Если это так, функция вызывает функцию writeback_
inodesO, чтобы принудительно записать на диск несколько десят-
десятков страниц (см. разд. "Поиск грязных страниц для записи на диск"
главы 15);
• вызывает функцию condreschedo, чтобы проверить флаг tif_need_
resched текущего процесса и, если флаг установлен, вызвать функцию
schedule().
9. Итак, все страницы файла, вовлеченные в операцию записи, обработаны.
Функция обновляет значение *ppos, чтобы установить указатель сразу по-
после последнего записанного символа.
10. Устанавливает ПОЛе current->backing_dev_info В NULL (СМ. шаг 3).
11. Завершает работу, возвращая количество фактически записанных байтов.
Методы prepare_write и commit^write для обычных файлов
Методы prepare_write И commit_write объекта address_space специализируют
общую операцию записи, реализованную функцией genericfiiewriteo для
обычных файлов и файлов блочных устройств. Оба они вызываются один раз
для каждой страницы файла, которая участвует в операции записи.
Каждая дисковая файловая система определяет свой собственный метод
preparewrite. Как и в случае с операцией чтения, метод является интерфей-
интерфейсом к общей функции. Например, в файловой системе Ext2 метод
preparewrite обычно реализован следующей функцией:
int ext2_prepare_write(struct file *file, struct page *page,
unsigned from, unsigned to)
{
return block_prepare_write(page, from, to, ext2_get_block);
}
Функция ext2_get_biock () уже упоминалась ранее в разд. "Чтение файла'1.
Она преобразует номер блока в файле в логический номер блока, представ-
представляющий позицию данных в физическом блочном устройстве.
Функция biockpreparewrite () подготавливает буферы и головы буферов
страницы файла, для чего выполняет следующие шаги:
1. Проверяет, является ли страница страницей буферов (установлен ли флаг
PGPrivate). ЕСЛИ флаг сброшен, фуНКЦИЯ ВЫЗЫВает фунКЦИЮ create_
emptybuf f ers (), чтобы выделить головы для всех буферов, включенных
в страницу (см. разд. "Страницы буферов" главы 15).
2. Для каждой головы буфера, входящего в страницу и затронутого операци-
операцией записи, функция выполняет следующие действия:
• сбрасывает флаг BHNew, если он установлен;
• если флаг BHMapped не установлен, функция выполняет следующие до-
дополнительные действия:
D вызывает специфичную для файловой системы функцию, адрес ко-
которой— getbiock был получен в качестве параметра. Вызванная
функция выполняет поиск среди дисковых структур файловой сис-
системы и находит логический номер блока для буфера (это номер от-
относительно начала раздела диска, но не начала обычного файла).
Функция, специфичная для файловой системы, сохраняет этот номер
в поле bbiocknr соответствующей головы буфера и устанавливает
ее флаг BHMapped. Функция getbiock может выделить новый физи-
физический блок для файла (например, если затребованный блок попада-
попадает в "дыру" в обычном файле; см. разд. "Дыры в файлах" главы 18).
В этом случае она устанавливает флаг BHNew;
п проверяет значение флага BHNew. Если он установлен, вызывает
функцию uranapunderlyingmetadata (), чтобы Проверить, содержит
ли какая-нибудь страница буферов блочного устройства в кэше
страниц буфер, ссылающийся на тот же блок на диске. Эта функция
вызывает функцию f indgetbiock () для поиска старого блока в
кэше страниц (см. разд. "Поиск блоков в кэше страниц" главы 15).
Если такой блок обнаруживается, функция сбрасывает его флаг
BHDirty и ждет окончания операции ввода/вывода для этого буфера.
Кроме того, если операция записи не переписывает целый буфер в
странице, функция заполняет нулями не переписанный участок бу-
буфера. Затем она переходит к рассмотрению следующего буфера на
странице;
• если операция записи не переписывает целый буфер, а флаги BHDelay и
вн Uptodate не установлены (то есть блок был выделен в структурах
файловой системы, а буфер в оперативной памяти не содержит кор-
корректный образ данных), функция вызывает функцию lirwbiocko для
блока, чтобы прочитать его содержимое с диска (см. разд. "Передача
голов буферов общему слою работы с блочными устройствами4 гла-
главы 15).
3. Блокирует текущий процесс, пока все операции чтения, запущенные на
шаге 2, не будут завершены.
4. Возвращает 0.
Когда меТОД prepare_write ВОЗВращает управление, функция generic_file_
write () обновляет страницу, записывая в нее данные, хранящиеся в адресном
пространстве режима пользователя. Затем она вызывает метод coramitwrite
объекта address_space. Этот метод реализуется функцией generic_cornmit_
write () почти во всех дисковых не журналируемых файловых системах.
ФунКЦИЯ genericcoramitwrite () ВЫПОЛНЯет Следующие ДеЙСТВИЯ!
1. Вызывает функцию biock_coinmit_write (), которая выполняет следую-
следующие действия:
• рассматривает все буферы на странице, затронутые операцией записи.
Для каждого буфера устанавливает флаги BHUptodate и BHjDirty его
головы;
• помечает соответствующий индексный дескриптор как "грязный". Как
было показано в главе 75, для этого может потребоваться занесение ин-
индексного дескриптора в список "грязных" индексных дескрипторов су-
суперблока;
• если ни один из буферов на странице не является устаревшим, устанав-
устанавливает флаг PGuptodate для страницы;
• устанавливает флаг PGdirty и помечает страницу как грязную в базис-
базисном дереве (см. разд. "Базисное дерево"главы 15).
2. Проверяет, увеличился ли файл в результате операции записи. В этом слу-
случае функция обновляет поле isize индексного дескриптора этого файла.
3. Возвращает 0.
Методы prepare_write и commit_write
для файлов блочных устройств
Операции записи в файлы блочных устройств очень похожи на соответст-
соответствующие операции для обычных файлов. Метод preparewrite объекта
addressspace у файлов бл очных устройств обычно реализуется следующей
функцией:
int blkdev_prepare_write(struct file *file, struct page *page,
unsigned from, unsigned to)
{
return block_prepare_write(page, from, to, blkdev_get_block);
}
Как ВИДИТе, Она ЯВЛЯеТСЯ Интерфейсом К фунКЦИИ block_prepare_write(),
описанной в предыдущем разделе. Естественно, есть отличие во втором па-
параметре, который указывает на функцию, преобразующую номер блока отно-
относительно начала файла в логический номер блока относительно начала блоч-
блочного устройства. Вспомним, что у файлов блочных устройств эти два номера
совпадают. (В разд. "Чтение из файла"ранее в этой главе обсуждается функ-
функция blkdev_get_block ().)
Метод commitwrite для файлов блочных устройств реализуется простой ин-
интерфейсной функцией:
int blkdev_commit_write(struct file *file, struct page *page,
unsigned from, unsigned to)
{
return block_commit_write(page, from, to);
}
Нетрудно заметить, что метод commitwrite для файлов блочных устройств
делает, в сущности, то же самое, что метод commitwrite для обычных файлов
(мы описали функцию biockcommitwrite () в предыдущем разделе). Единст-
Единственное отличие заключается в том, что метод не проверяет, увеличился ли
файл в результате операции записи: вы не можете увеличить файл блочного
устройства, добавляя символы в его конец.
Запись грязных страниц на диск
Результат работы системного вызова write о заключается в изменении со-
содержимого некоторых страниц в кэше, возможно, с выделением новых стра-
страниц и добавлением их в кэш, если их там не было. В некоторых случаях (на-
(например, если файл был открыт с флагом osync) пересылка данных начинает-
начинается немедленно (СМ. шаг 6 В Описании фуНКЦИИ generic_file_write() В
разд. "Запись в файл"ранее в этой главе). Впрочем, как правило, ввод/вывод
данных откладывается, что было подробно обсуждено в разд. "Запись гряз-
грязных страниц на диск" в главе 15.
Когда ядро хочет запустить фактическую пересылку данных, оно, в конечном
счете, вызывает метод writepages объекта addressspace этого файла, а метод
ищет грязные страницы в базисном дереве и записывает их на диск. Напри-
Например, в файловой системе Ext2 метод writepages реализован следующей функ-
функцией:
int ext2_writepages(struct address space *mapping,
struct writeback_control *wbc)
{
return mpage_writepages(mapping, wbc, ext2_get_block);
}
Нетрудно заметить, что эта функция является интерфейсом к функции обще-
общего назначения mpagewritepages о. На практике, если в файловой системе ме-
метод writepages не определен, напрямую ядро вызывает функцию
mpagewritepages (), передавая ей null в качестве третьего аргумента. Функ-
Функция ext2_get_biock (), уже упоминавшаяся в разд. "Чтение из файла"ранее в
этой главе, является специфичной для файловой системы и преобразует но-
номер блока в файле в логический номер блока.
Структура writebackcontroi является дескриптором, уточняющим, как
должна выполняться операция обратной записи на диск; мы уже описывали
ее в главе 15.
ФуНКЦИЯ mpagewritepages () ВЫПОЛНЯет следующие действия:
1. Если очередь запросов переполнена запросами на запись, а процесс не
должен блокироваться, функция возвращает управление, ничего не запи-
записав на диск.
2. Определяет первую страницу, подлежащую рассмотрению. Если дескрип-
дескриптор writebackcontroi задает начальную позицию в файле, функция пре-
преобразует ее в индекс страницы. В противном случае, если дескриптор
writebackcontroi отмечает, что процесс не хочет ждать окончания пере-
пересылки данных, функция устанавливает индекс первой страницы равным
значению, которое хранится в поле mapping->writeback_index (то есть пе-
перебор страниц начнется с последней страницы, рассмотренной в преды-
предыдущей операции обратной записи). Наконец, если процесс должен ждать
окончания пересылки данных, перебор страниц начинается с первой стра-
страницы файла.
3. Вызывает функцию find_get_pages_tag(), чтобы наЙТИ дескрипторы гряз-
ных страниц в кэше (см. разд. "Теги базисного дерева" главы 15).
4. Для каждого дескриптора страницы, полученного на предыдущем шаге,
функция выполняет следующие действия:
• вызывает функцию lockpage (), чтобы заблокировать страницу;
• проверяет, что страница по-прежнему корректна и находится в кэше
страниц (потому что другой управляющий тракт ядра мог воздейство-
воздействовать на страницу между шагами 3 и 4);
• проверяет флаг PGwriteback этой страницы. Если он установлен, зна-
значит, страница уже записывается на диск. Если процесс должен ждать
окончания пересылки данных, функция вызывает функцию wait_on_
page bit о, чтобы заблокировать текущий процесс, пока не будет
сброшен флаг pg writeback; когда функция закончит работу, любая ра-
ранее запущенная операция обратной записи заканчивается. В противном
случае, если процесс не хочет ждать, функция проверяет флаг PGdirty.
Если он уже сброшен, значит, о странице позаботится текущая опера-
операция обратной записи. Поэтому функция разблокирует страницу и пере-
переходит назад, к шагу 4, чтобы продолжить со следующей страницы;
• если параметр getbiock равен null (метод writepages не определен),
функция вызывает метод mapping->writepage объекта addressspace
данного файла, чтобы записать страницу на диск. В противном случае,
если параметр getbiock не равен null, функция вызывает функцию
mpagewritepage (). Подробности даны в описании шага 8.
5. Вызывает функцию condreschedo, чтобы проверить состояние флага
tifneedresched текущего процесса и, если флаг установлен, вызвать
ФУНКЦИЮ schedule ().
6. Если функция перебрала не все страницы в заданном диапазоне, или коли-
количество страниц, фактически записанных на диск, меньше исходного зна-
значения, заданного дескриптором writebackcontroi, функция переходит
назад, к шагу 3.
7. Если дескриптор writebackcontroi не задает первоначальную позицию в
файле, функция записывает в поле mapping->writeback_index индекс по-
последней рассмотренной страницы.
8. Если на шаге 4 была вызвана функция mpagewritepage (), и она возвратила
адрес дескриптора bio, описываемая функция вызывает функцию
mpage_bio_submit ().
Типичная файловая система, такая как Ext2, реализует метод writepage в виде
интерфейса к функции общего назначения biock_write_fuii_page(), переда-
передавая ей в качестве параметра адрес специфичной для файловой системы функ-
функции getbiock. Функция blockwritefullpage () ВО МНОГОМ аналогична
функции biock_read_fuii_page(), описанной ъ разд. "Чтение из файла11 ранее
в этой главе\ она выделяет головы буферов для страницы (если страница еще
не была страницей буферов) и вызывает функцию submitbho для каждой
головы, задавая в качестве операции write. Что касается файлов блочных
устройств, для них метод writepage реализован с помощью функции
blkdevwritepage (), КОТОрая ЯВЛЯетСЯ Интерфейсом ДЛЯ функции block_write_
full_page().
Многие не журналируемые файловые системы используют функцию
mpagewritepage (), а не специализированный метод writepage. Это повышает
производительность, поскольку функция mpagewritepage () старается выпол-
выполнять пересылку данных, собрав как можно больше страниц в одном дескрип-
дескрипторе Ыо. Такой подход позволяет драйверам блочных устройств использо-
использовать возможности современных дисковых контроллеров по DMA-пересылке
вразброс.
Короче говоря, функция mpagewritepage () проверяет, содержит ли страница,
подлежащая записи, блоки, не смежные на диске, содержит ли эта страница
"дыру" файла, а также является ли какой-нибудь блок на этой странице "не
грязным" или не устаревшим. Если выполняется хотя бы одно из этих усло-
условий, функция прибегает к помощи зависящего от файловой системы метода
writepage, как описано ранее. В противном случае функция добавляет стра-
страницу в качестве сегмента к дескриптору bio. Адрес дескриптора bio переда-
передается функции в качестве параметра. Если передано значение null, функция
nipagewritepage () инициализирует новый дескриптор bio и возвращает его
адрес вызывающей функции, которая, в свою очередь, передает его обратно
функции nipagewritepage (), когда вызывает ее в следующий раз. Таким спо-
способом достигается добавление нескольких страниц в один bio. Если страница
не является смежной к последней добавленной странице, функция
mpagewritepage () вызывает фунКЦИЮ mpagebiosubmit (), чтобы ЗапусТИТЬ
пересылку данных для этого bio, а сама выделяет для страницы новый bio.
Функция mpagebiosubmit () устанавливает метод biendio в адрес функции
mpageendiowrite (), затем вызывает функцию submitbio (), чтобы запус-
тить пересылку данных (см. разд. "Передача голов буферов общему слою ра-
работы с блочными устройствами'1 главы 15). После успешного окончания пе-
пересылки данных функция завершения mpageendiowrite () "будит" процес-
процессы, ожидающие завершения пересылки, и уничтожает дескриптор bio.
Отображение в память
Как было сказано в главе 9, область памяти может быть ассоциирована с не-
некоторым фрагментом обычного файла дисковой файловой системы или файла
блочного устройства. Это означает, что обращение к байту внутри страницы
области памяти преобразуется ядром в операцию над соответствующим бай-
байтом файла. Эта методика называется отображением в память.
Существует два вида отображения в память:
□ совместно используемое — каждая операция записи в отношении страниц
области памяти изменяет содержимое файла на диске. Более того, если
процесс пишет в страницу совместно используемого отображения, изме-
изменения становятся видны всем остальным процессам, отображающим в па-
память этот файл;
□ закрытое — предполагается к использованию в ситуациях, когда процесс
создает отображение только для чтения файла, а не для записи в него. Для
таких целей закрытое отображение гораздо эффективнее совместно ис-
используемого. Зато каждая операция записи в страницу закрытого отобра-
отображения приведет к прекращению отображения страницы в файл. Таким об-
образом, запись не приводит к изменению файла на диске, и никакие изме-
изменения не видны другим процессам, обращающимся к этому файлу. Однако
страницы закрытого отображения, не измененные процессом, отражают
изменения, внесенные в файл другими процессами.
Процесс может создать новое отображение в память с помощью системного
вызова mmapo (см. разд. "Создание отображения в память" далее в этой
главе). Программист должен задать в качестве параметра системного вызова
флаг mapshared или mapprivate. Нетрудно догадаться, что в первом случае
будет создано совместно используемое отображение, а во втором — закры-
закрытое. Когда отображение создано, процесс может получать данные, хранящие-
хранящиеся в файле, читая их из ячеек новой области памяти. Если отображение в па-
память является совместно используемым, процесс может изменять соответст-
соответствующий файл, записывая информацию в те же ячейки памяти. Чтобы
уничтожить или сжать отображение, процесс может воспользоваться систем-
системным вызовом munmapO (см. разд. "Уничтожение отображения в память"
далее в этой главе).
В качестве общего правила можно утверждать, что если отображение в па-
память является совместно используемым, у соответствующей области памяти
установлен флаг vmshared; у закрытого отображения он сброшен. Как мы
увидим впоследствии, исключение из этого правила существует для совмест-
совместно используемых отображений, созданных только для чтения.
Структуры отображения в память
Отображение в память представляется комбинацией следующих структур:
□ индексного дескриптора, ассоциированного с отображаемым файлом;
□ объекта addressspace отображаемого файла;
□ файлового объекта для каждого отображения, созданного для файла раз-
различными процессами;
□ дескриптора vmareastruct для каждого отдельного отображения файла;
□ дескриптора страницы для каждого страничного кадра, присвоенного об-
области памяти, которая отображает файл.
На рис. 16.2 изображена связь между этими структурами. В левой части ри-
рисунка мы видим индексный дескриптор, идентифицирующий файл. Поле
imapping КЗЖДОГО ИНДеКСНОГО дескриптора указывает на объект address_
space файла. Поле pagetree каждого объекта addressspace указывает на ба-
базисное дерево страниц, принадлежащих этому адресному пространству (см.
разд. "Базисное дерево" главы 15), в то время как поле immap указывает на
другое дерево, называемое базисным деревом приоритетного поиска облас-
областей памяти, которое принадлежит этому адресному пространству. Основное
предназначение базисного дерева приоритетного поиска состоит в выполне-
выполнении "обратного отображения", т. е. в быстром нахождении всех процессов,
совместно использующих заданную страницу. Деревья приоритетного поиска
подробно описываются в следующей главе, поскольку они применяются для
утилизации страничных кадров. Связь между файловыми объектами, отно-
относящимися к одному файлу, и индексным дескриптором устанавливается при
ПОМОЩИ ПОЛЯ fmapping.
Рис. 16.2. Структуры для отображения файла в память
Каждый дескриптор области памяти имеет поле vm_f ile, которое связывает
его с файловым объектом отображенного файла (если это поле равно null,
значит, область памяти не используется в отображении). Позиция начала ото-
отображения хранится в поле vmpgof f дескриптора области памяти, представля-
представляет собой смещение в файле, измеряемое в единицах, равных длине страницы.
Длина отображенной порции файла — это просто длина области памяти, ко-
которая ВЫЧИСЛЯетСЯ ПО значениям ПОЛеЙ vm_start И vm_end.
Страницы совместно используемых отображений в память всегда находятся в
кэше страниц; страницы закрытых отображений находятся в кэше до тех пор,
пока не будут изменены. Когда процесс пытается модифицировать страницу
закрытого отображения в память, ядро создает копию страничного кадра и
заменяет оригинал на дубликат в Таблице Страниц процесса. Это одно из
применений механизма "копирования при записи", обсуждаемого в главе 8.
Оригинальный страничный кадр остается в кэше страниц, хотя он больше не
принадлежит отображению в память, поскольку заменен дубликатом. Зато
дубликат не заносится в кэш, поскольку он больше не содержит данные,
представляющие файл на диске.
На рис. 16.2 также показано несколько дескрипторов страниц, находящихся в
кэше и относящихся к файлу, отображенному в память. Обратите внимание,
что первая область памяти на рисунке имеет размер три страницы, но для нее
выделено только два страничных кадра. Можно предположить, что процесс,
владеющий этой областью памяти, никогда не обращался к третьей странице.
Ядро предлагает несколько технических средств для специализации меха-
механизма отображения в память для любой файловой системы. Самая важная
часть реализации отображения в память делегирована методу файлового объ-
объекта, названному mmap. В большинстве дисковых файловых систем и для фай-
файлов блочных устройств этот метод реализуется функцией общего назначения
generic file ramap (), которая описана в следующем разделе.
Отображение файла в память основано на механизме выделения страниц по
требованию, описанного в главе 9. Фактически только что установленное
отображение в память является областью памяти, не содержащей ни одной
страницы. Когда процесс обращается по адресу внутри этой области, возни-
возникает событие "ошибка при обращении к странице". Обработчик этого собы-
события проверяет, определен ли метод nopage для этой области памяти. Если ме-
метод не определен, значит, область памяти не отображает никакого файла.
В противном случае отображение имеет место, и метод несет ответственность
за чтение страниц, для чего обращается к блочному устройству. Почти во
всех дисковых операционных системах и файлах блочных устройств метод
nopage реализован С ПОМОЩЬЮ функции f ilemapnopage ().
Создание отображения в память
Чтобы создать новое отображение в память, процесс делает системный вызов
mmap (), передавая ему следующие параметры:
□ дескриптор файла, идентифицирующий файл, который должен быть ото-
отображен;
□ смещение внутри файла, определяющее первый символ отображаемого
фрагмента файла;
□ длину отображаемого фрагмента файла;
□ набор флагов. Процесс должен явно установить флаг mapshared или
mapprivate, чтобы уточнить тип запрошенного отображения;
( Примечание )
Процесс может также установить флаг map_anonymous, помечая новую область
памяти как анонимную, т. е., не ассоциированную ни с каким файлом на диске
(см. главу 9). Процесс может также создать область памяти с установленными
флагами mapshared и mapanonymous. В этом случае область будет отобра-
отображать специальный файл в файловой системе tmpfs (см. разд. "Совместно ис-
используемая память межпроцессного взаимодействия" главы 19), который бу-
будет доступен всем потомкам процесса.
□ набор прав доступа, определяющих один или несколько типов доступа
к области памяти: для чтения (protread), записи (protwrite) или выпол-
выполнения (prot_exec);
□ линейный адрес (необязательно), который воспринимается ядром как под-
подсказка, с какого места должна начинаться новая область памяти. Если ус-
установлен флаг mapfixed, а ядро не может выделить новую область памяти,
начинающуюся с заданного линейного адреса, системный вызов заверша-
завершается неудачей.
Системный вызов mmapo возвращает линейный адрес первой ячейки новой
области памяти. Из соображений совместимости в архитектуре 80x86 ядро
резервирует две точки входа для mmap () в таблице системных вызовов: одну с
индексом 90, а другую— с индексом 192. Первая соответствует служебной
процедуре oidmmap () (используемой в старых библиотеках С), а вторая —
служебной процедуре sys_mmap2 () (используемой в новых библиотеках С).
Эти две служебные процедуры различаются только способом передачи пара-
параметров системному вызову. Обе вызывают функцию dommappgof f (), опи-
описанную в разд. "Выделение интервала линейных адресов" главы 9. Сейчас мы
дополним это описание, уточнив только шаги, выполняемые при создании
области памяти, отображающей файл. Мы опишем случай, в котором пара-
параметр file (указатель на файловый объект) функции dommappgof f () не равен
null. Ради ясности изложения, мы сохраним нумерацию шагов описания
функции dommappgof f () и укажем, какие дополнительные действия совер-
совершаются в новой ситуации:
□ Шаг 1 — проверяет, определена ли файловая операция mmap для отобра-
отображаемого файла. Если нет, возвращает код ошибки. Значение null для mmap
в таблице означает, что файл не может быть отображен (например, потому
что является каталогом).
□ Шаг 2 — функция get_unmapped_area () вызывает метод get_unmapped_area
файлового объекта (если метод определен), чтобы выделить интервал
линейных адресов, подходящий для отображения файла в память. В дис-
дисковых файловых системах этот метод не определен. В таком случае, как
было сказано в разд. "Работа с областями памяти" главы 9, функция
get_unmapped_area () вызывает метод get_unmapped_area дескриптора па-
мяти.
□ Шаг 3 — в дополнение к обычным проверкам на непротиворечивость
функция сравнивает тип запрошенного отображения в память (заданный
параметром flags системного вызова mmapo) и флаги, заданные при от-
открытии файла (которые хранятся в поле f iie->f_mode). В частности:
• если затребовано совместно используемое отображение с правами на
запись, функция убеждается, что файл был открыт для записи и что он
был открыт не в режиме добавления (флаг oappend системного вызова
open ());
• если затребовано совместно используемое отображение, функция убе-
убеждается, что на файл не наложена обязательная блокировка (см.
разд. "Блокировка файлов"главы 12);
• при любом типе отображения в память функция убеждается, что файл
был открыт для чтения.
Если хотя бы одно из этих условий нарушено, возвращается код ошибки.
Кроме того, при инициализации значения в поле vm_f lags дескриптора но-
новой области памяти функция устанавливает флаги vmread, vmwrite,
VM_EXEC, VM_SHARED, VMJMAYREAD, VM_MAYWRITE, VMJ4AYEXEC И VM_MAYSHARE В CO-
ответствии с правами доступа к файлу и типом запрошенного отображе-
отображения в память (см. разд. "Права доступа к области памяти" главы 9). В це-
целях оптимизации флаги vmshared и vmmaywrite сбрасываются для совме-
совместно используемого отображения без права на запись. Это можно сделать,
поскольку процессу не разрешено записывать в страницы области памяти,
и, следовательно, такое отображение не отличается от закрытого. Впро-
Впрочем, ядро разрешает другим процессам обращаться к тому же файлу, что-
чтобы читать страницы из этой области памяти.
□ Шаг 10— инициализирует поле vmfiie дескриптора области памяти ад-
адресом файлового объекта и увеличивает счетчик обращений файла. Вызы-
Вызывает метод mmap для отображаемого файла, передавая в качестве парамет-
параметров адрес файлового объекта и адрес дескриптора области памяти.
В большинстве файловых систем этот метод реализован функцией
genericf ilemmap (), которая выполняет следующие действия:
• сохраняет текущее время в поле iatime индексного дескриптора файла
и помечает индексный дескриптор как "грязный";
• инициализирует поле vmops дескриптора области памяти адресом таб-
таблицы genericf iievmops. Все методы в этой таблице равны null, кро-
ме метода nopage, реализованного функцией filemapnopage о, и метода
populate, реализованного функцией fiiemap_popuiate() (см. разд. "Не-
"Нелинейные отображения в память" далее в этой главе).
□ Шаг 11 — увеличивает поле iwritecount индексного дескриптора файла,
т. е. счетчик обращений для пишущих процессов.
Уничтожение отображения в память
Когда процесс готов уничтожить отображение в память, он делает системный
вызов munmap (). Этот же системный вызов можно использовать для уменьше-
уменьшения размера любой области памяти. Вызов принимает следующие параметры:
□ адрес первой ячейки удаляемого интервала линейных адресов;
□ длину удаляемого интервала линейных адресов.
Служебная процедура sysmunmap() этого системного вызова использует
функцию domunmap (), описанную в разд. "Освобождение интервала линей-
линейных адресов" в главе 9. Обратите внимание, что в этом случае нет необходи-
необходимости принудительно записывать на диск содержимое страниц совместно
используемого отображения в память, которое должно быть уничтожено. Эти
страницы продолжают действовать как кэш диска, поскольку они по-
прежнему входят в состав кэша страниц.
Выделение страниц по требованию
для отображения в память
По соображениям эффективности, страничные кадры назначаются отображе-
отображению не сразу после его создания, а в самый последний момент, т. е. когда
процесс уже пытается обратиться к странице, тем самым возбуждая исключе-
исключение "ошибка при обращении к странице".
В главе 9 мы видели, как ядро проверяет, входит ли адрес, вызвавший ошиб-
ошибку, в какую-нибудь область памяти, принадлежащую процессу. Если так оно
и есть, ядро проверяет запись в Таблице Страниц, соответствующую этому
адресу, и вызывает функцию donopage (), если запись содержит null.
Функция donopage () выполняет действия, общие для всех механизмов вы-
выделения страниц по требованию, например, выделение страничного кадра и
обновление Таблиц Страниц. Она также проверяет, определен ли метод
nopage для области памяти, вовлеченной в эту ситуацию. В разд. "Выделение
страниц по требованию" главы 9 мы описали случай, когда метод не опреде-
лен (анонимная область памяти); сейчас мы дополним то описание действия-
действиями, выполняемыми функцией, когда метод определен:
1. Вызывает метод nopage, который возвращает адрес страничного кадра, со-
содержащего запрошенную страницу.
2. Если процесс пытается сделать запись в страницу, а отображение в память
является закрытым, функция должна избежать следующего исключения,
связанного с "копированием при записи", для чего делает копию только
что прочитанной страницы и заносит ее в неактивный список страниц (см.
главу 17). Если область закрытого отображения еще не имеет подчиненной
анонимной области памяти, содержащей новую страницу, функция либо
добавляет новую подчиненную анонимную область, либо расширяет уже
существующую (см. разд. Работа с областями памяти" главы 9). На
следующих шагах функция пользуется новой страницей, а не страницей,
возвращенной методом nopage, чтобы последняя не была модифицирована
процессом, работающим в режиме пользователя.
3. Если какой-то другой процесс выполнил усечение страницы или сделал ее
недействительной (для подобной проверки используется поле
t runca tecount ДеСКрИПТОра addressspace), функция повторяет ПОПЫТКУ
получить страницу, вернувшись к шагу 1.
4. Увеличивает поле rss дескриптора памяти процесса, чтобы отметить факт
назначения процессу нового страничного кадра.
5. Заносит в запись Таблицы Страниц, соответствующую адресу, вызвавше-
вызвавшему ошибку, адрес страничного кадра и права доступа к странице, храня-
хранящиеся в поле vmpageprot области памяти.
6. Если процесс пытается выполнить запись в страницу, функция устанавли-
устанавливает биты Read/write и Dirty в Таблице Страниц. В этом случае либо стра-
страничный кадр назначен исключительно данному процессу, либо страница
является совместно используемой. Как бы то ни было, запись в нее должна
быть разрешена.
Суть алгоритма выделения страниц по требованию заключается в методе
nopage области памяти. Вообще говоря, он должен возвращать адрес странич-
страничного кадра, содержащего страницу, к которой обратился процесс. Реализация
метода зависит от типа области памяти, содержащей страницу.
При работе с областями памяти, отображающими файлы, метод nopage дол-
должен вначале поискать запрошенную страницу в кэше. Если страница не будет
обнаружена, метод должен прочитать ее с диска. В большинстве файловых
СИСТеМ меТОД nopage реализован С ПОМОЩЬЮ функции filemap_nopage(), KOTO-
рая принимает три параметра:
□ area— адрес дескриптора области памяти, содержащей запрошенную
страницу;
□ address — линейный адрес запрошенной страницы;
□ type — указатель на переменную, в которую функция будет записывать
тип ошибки обращения к странице, который определила функция
(VM_FAULT_MAJOR ИЛИ VM_FAULT_MINOR).
Функция fiiemapnopage о выполняет следующие действия:
1. Получает адрес file файлового объекта из поля area->vm_fiie и затем ад-
адрес объекта address_space ИЗ ПОЛЯ file->f_mapping. Получает адрес ИН-
дексНОГО Дескриптора ИЗ ПОЛЯ host объекта addressspace.
2. С помощью полей vmstart и vmpgof f области area определяет смещение
внутри файла для данных, соответствующих странице, начинающейся с
адреса address.
3. Проверяет, не превышает ли смещение размер файла. Если это произой-
произойдет, функция возвратит null, что означает сбой в выделении новой стра-
страницы, если только ошибка обращения к странице не была вызвана отлад-
отладчиком, отслеживающим другой процесс с помощью системного вызова
ptrace (). Но мы не будем обсуждать этот специальный случай.
4. Если флаг vmrandread области памяти установлен, можно предположить,
что процесс читает страницы отображения произвольным образом. Функ-
Функция не прибегает к опережающему чтению и переходит к шагу 10.
5. Если флаг vmseqread области памяти установлен, можно предположить,
что процесс читает страницы отображения строго последовательно. Функ-
Функция вызывает функцию page_cache_readahead(), чтобы ВЫПОЛНИТЬ ОПере-
жающее чтение, начиная со страницы, вызвавшей ошибку (см.
разд. "Опережающее чтение файлов11 ранее в этой главе).
6. Вызывает функцию f indgetpage (), чтобы искать в кэше страницу, иден-
идентифицируемую объектом addressspace и смещением внутри файла. Если
страница найдена, функция переходит к шагу 11.
7. Если функция находится на этом шаге, значит, страница в кэше не найде-
найдена. Функция проверяет флаг vmseqread области памяти:
• если флаг установлен, значит, ядро агрессивно выполняет опережаю-
опережающее чтение страниц области памяти, и алгоритм опережающего чтения
себя не оправдал. Функция вызывает функцию handieramiss о для
настройки параметров опережающего чтения (см. разд. "Опережающее
чтение файлов"ранее в этой главе), а затем переходит к шагу 10;
• в противном случае, если флаг сброшен, функция увеличивает на еди-
единицу значения счетчика rnmapmiss в дескрипторе f ilerastate файла.
Если количество промахов намного превышает количество попаданий
(хранящееся в счетчике mmapjiit), функция пренебрегает опережающим
чтением и переходит к шагу 10.
8. Если опережающее чтение не отключено (поле rapages дескриптора
filerastate больше нуля), функция вызывает функцию do_page_cache_
readahead (), чтобы прочитать некоторое количество страниц, окружаю-
окружающих запрошенную.
9. Вызывает функцию findgetpage(), чтобы проверить наличие запро-
запрошенной страницы в кэше. Если страница там, функция переходит к ша-
шагу 11.
10. Вызывает функцию pagecacheread (). Эта функция проверяет, находится
ли запрошенная страница в кэше страниц, и, если ее там нет, выделяет
новый страничный кадр, добавляет его в кэш страниц и выполняет метод
mapping->a_ops->readpage, чтобы спланировать операцию ввода/вывода,
которая прочитает содержимое страницы с диска.
11. Вызывает фуНКЦИЮ grab_swap_token(), Чтобы, ПО ВОЗМОЖНОСТИ, ПрИСВО-
ить жетон защиты от выгрузки текущему процессу (см. главу 17).
12. Запрошенная страница находится в кэше. Функция увеличивает на еди-
единицу значение счетчика mmaphit дескриптора файла f ilerastate.
13. Если страница устарела (флаг PGuptodate сброшен), функция вызывает
функцию lockpageo, чтобы заблокировать страницу, выполняет метод
mapping->a_ops->readpage, чтобы запустить пересылку данных, И ВЫЗЫВа-
ет функцию waitonpagebito, чтобы приостановить процесс, пока
страница не будет разблокирована, т. е. пока не завершится операция
пересылки данных.
14. Вызывает функцию markpageaccessedo, чтобы пометить страницу как
страницу, к которой обращался процесс (см. следующую главу).
15. Если в кэше была найдена неустаревшая версия страницы, функция запи-
записывает в переменную *type значение vmfaultminor; в противном случае
записывает в нее значение vmfaultmajor.
16. Возвращает адрес запрошенной страницы.
Процесс, работающий в режиме пользователя, может настроить опережаю-
опережающее чтение, выполняемое функцией f ilemapnopage (), с помощью системного
вызова madviseo. Команда madv_random устанавливает флаг vm_rand_read об-
области памяти, чтобы показать, что процесс будет обращаться к страницам
области памяти в случайном порядке. Команда madvsequential устанавлива-
устанавливает флаг vmseqread, чтобы показать, что обращение к страницам будет строго
последовательным; а команда madvnormal сбрасывает флаги vmrandread и
vmseqread, показывая, что порядок обращения будет непредсказуемым.
Принудительная запись на диск
грязных страниц отображения в память
Системный вызов msync () может быть использован процессом для сброса на
диск грязных страниц, принадлежащих совместно используемому отображе-
отображению в память. В качестве параметров системный вызов принимает начальный
адрес интервала линейных адресов, длину этого интервала и набор флагов,
имеющих следующие значения:
□ mssync — этот флаг просит системный вызов приостановить процесс до
завершения операции ввода/вывода. Таким образом, вызывающий процесс
может предполагать, что, когда системный вызов закончит работу, все
страницы отображения в память уже будут записаны;
□ msasync (дополнительный к mssync) — этот флаг просит системный вы-
вызов вернуть управление немедленно, не приостанавливая вызвавший про-
процесс;
П1 ms invalidate — этот флаг просит системный вызов отменить другие ото-
отображения в память того же файла (что на самом деле не реализовано, по-
поскольку в Linux это было бы бесполезно).
Служебная процедура sys_msync() вызывает функцию msync_interval () ДЛЯ
каждой области памяти, входящей в интервал линейных адресов. Эта функ-
функция выполняет следующие действия:
1. Если поле vmfile дескриптора области памяти равно null, или флаг
vmshared сброшен, функция возвращает 0 (область памяти не является
совместно используемым отображением файла в память с правами на
запись).
2. Вызывает функцию f iiemapsync (), которая просматривает записи в Таб-
Таблице Страниц, соответствующие интервалам линейных адресов для дан-
данной области памяти. Для каждой найденной страницы функция сбрасыва-
сбрасывает флаг Dirty в соответствующей записи в таблице страниц и вызывает
функцию f lushtibpage () для очистки соответствующих TLB-буферов.
Затем она устанавливает флаг PGdirty в дескрипторе страницы, чтобы
пометить страницу как грязную.
3. Если флаг msasync установлен, функция возвращает управление. То есть
практический эффект от флага msasync заключается в установке флагов
PGdirty для страниц в области памяти; системный вызов не запускает пе-
пересылку данных.
4. Если функция дошла до этого шага, значит, флаг mssync установлен. Сле-
Следовательно, функция должна записать на диск страницы области памяти и
приостановить текущий процесс до окончания пересылки данных. Чтобы
сделать это, она получает семафор isem индексного дескриптора файла.
5. Вызывает функцию f iiemapfdatawrite (), которая принимает адрес объек-
объекта addressspace данного файла. Вызванная функция настраивает дескрип-
дескриптор writebackcontroi, задав ему режим синхронизации wbsyncall, и
проверяет, есть ли у адресного пространства встроенный метод
writepages. Если метод есть, функция вызывает функцию, реализующую
метод, и возвращает управление. В противном случае она вызывает функ-
функцию mpagewritepages () (см. разд. "Запись грязных страниц на диск11 ранее
в этой главе).
6. Проверяет, определен ли метод fsync для файлового объекта. Если опре-
определен, выполняет его. У обычных файлов этот метод, как правило, ограни-
ограничивается сбросом на диск индексного дескриптора файла. Однако у фай-
файлов блОЧНЫХ УСТРОЙСТВ ЭТОТ МеТОД ВЫЗЫВаеТ фуНКЦИЮ sync_blockdev(),
которая запускает пересылку данных.
7. Вызывает функцию f iiemapf datawait (). В разд. "Теги базисного дерева11 в
главе 15 было сказано, что базисное дерево в кэше страниц идентифици-
идентифицирует все страницы, записываемые на диск в данный момент, при помощи
тега pagecachetagwriteback. Функция быстро сканирует участок индекс-
индексного дерева, покрывающий заданный интервал линейных адресов, с целью
найти страницы, у которых установлен флаг pg writeback. Для каждой та-
такой страницы функция вызывает функцию waitonpagebit () и ждет, по-
пока флаг PGwriteback не будет сброшен, т. е. пока не завершится пересылка
данных этой страницы.
8. Освобождает семафор isem и завершает работу.
Нелинейные отображения в память
Ядро Linux 2.6 предлагает еще один способ обращения к обычному файлу —
нелинейные отображения в память. В принципе, нелинейное отображение в
память — отображение файла в память, описанное ранее, но страницы памя-
памяти не соответствуют последовательным страницам файла; каждая страница
памяти отображает произвольную страницу данных файла.
Конечно, приложение, работающее в режиме пользователя, может достичь
того же результата, многократно делая системный вызов mmap (), каждый раз
для новой 4096-байтовой порции файла. Однако такой подход не очень
эффективен при нелинейном отображении больших файлов, потому что для
каждой страницы отображения потребуется своя область памяти.
Чтобы поддержать нелинейное отображение в память, ядро применяет неко-
некоторые дополнительные структуры данных. Во-первых, флаг vmnonlinear де-
скриптора области памяти показывает, что эта область содержит нелинейное
отображение. Все дескрипторы областей памяти с нелинейными отображе-
отображениями для данного файла собраны в двунаправленном циклическом списке
С корнем В поле i_mmap_nonlinear объекта address_space.
Чтобы создать нелинейное отображение в память, приложение режима поль-
пользователя вначале создает нормальное совместно используемое отображение
с помощью системного вызова mmap(). Затем приложение переотображает
некоторые страницы из области памяти, делая системный вызов
remap_file_pages (). Служебная процедура sys_remap_file_pages() СИСТемно-
го вызова принимает четыре параметра:
□ start — линейный адрес внутри области памяти с совместно используе-
используемым отображением, к которому обращается вызывающий процесс;
□ size — размер переотображенной порции файла в байтах;
□ prot — не используется (должен быть ноль);
□ pgof f — индекс первой страницы из файла, которая должна быть переото-
переотображена;
□ flags — флаги, управляющие нелинейным отображением.
Служебная процедура переотображает порцию файла, идентифицируемую
параметрами pgof f и size, начиная с линейного адреса start. Если либо об-
область памяти не является совместно используемой, либо она недостаточно
велика, чтобы вместить все страницы, запрошенные отображением, систем-
системный вызов завершается неудачей и возвращает код ошибки. Короче говоря,
служебная процедура заносит область памяти в список immap nonlinear дан-
данного файла и вызывает метод populate области памяти.
Для всех обычных файлов метод populate реализован функцией fiiemap_
populate (), которая выполняет следующие действия:
1. Проверяет, сброшен ли флаг mapnonblock в параметре flags системного
ВЫЗОВа remapf ilepages (). ЕСЛИ сброшен, функция ВЫЗЫВает фуНКЦИЮ
dopagecachereadahead (), чтобы заранее прочитать страницы файла, ко-
которые должны быть переотображены.
2. Для каждой страницы, подлежащей переотображению, выполняет сле-
следующие действия:
• проверяет, включен ли дескриптор страницы в кэш страниц. Если еще
нет, а флаг mapnonblock сброшен, функция читает страницу с диска;
• если дескриптор страницы находится в кэше, функция обновляет
запись в Таблице Страниц для соответствующего линейного адреса так,
чтобы она указывала на страничный кадр. Кроме того, функция обнов-
обновляет счетчик страниц в дескрипторе области памяти;
• в противном случае, если дескриптора страницы нет в кэше, функция
сохраняет смещение страницы файла в старших 32 битах записи Таб-
Таблицы Страниц для соответствующего линейного адреса. Кроме того,
функция сбрасывает бит Present в записи в Таблице Страниц и уста-
устанавливает бит Dirty.
Как было сказано в главе 9, при обработке ошибки обращения к странице
функция handieptef auit () проверяет биты Present и Dirty в записи в Таб-
Таблице Страниц. Если их значения соответствуют нелинейному отображению в
ПаМЯТЬ, функция handieptef ault () ВЫЗЫВает функцию dofilepage (), КО-
торая извлекает индекс запрошенной страницы файла из старших битов запи-
записи в Таблице Страниц. Затем функция dofiiepageo вызывает метод
populate области памяти, чтобы прочитать страницу с диска и обновить саму
запись Таблицы Страниц.
Поскольку страницы нелинейного отображения находятся в кэше страниц в
соответствии с индексом страницы, показывающим ее положение относи-
относительно начала файла (а не с индексом, показывающим положение относи-
относительно начала области памяти), нелинейные отображения сбрасываются на
диск точно так же, как и линейные (см. разд. "Принудительная запись на диск
грязных страниц отображения в память"ранее в этой главе).
Прямой ввод/вывод
Как мы уже знаем, в Linux 2.6 нет существенной разницы между обращением
к обычному файлу через файловую систему, обращением к нему посредством
блоков, доступных через соответствующий файл блочного устройства и даже
обращением к файлу с использованием его отображения в память. Однако
существуют чрезвычайно сложные программы (так называемые приложения
с самокэшированием), которые стремятся иметь полный контроль над всем
механизмом ввода/вывода данных. Приведем в качестве примера высокопро-
высокопроизводительные серверы баз данных. В большинстве из них реализованы соб-
собственные механизмы кэширования, которые используют особые свойства за-
запросов к базам данных. Подобным программам кэш страниц, предоставляе-
предоставляемый ядром, мало поможет. Наоборот, он будет вреден по следующим
причинам:
□ большое количество страничных кадров будет непроизводительно дубли-
дублировать данные, уже находящиеся в оперативной памяти (в кэше памяти,
созданном пользователем);
П работа системных вызовов read () и write () будет замедляться из-за избы-
избыточных инструкций, обрабатывающих кэш страниц и выполняющих опе-
опережающее чтение; то же самое справедливо в отношении страничных опе-
операций, связанных с отображением файлов в память;
□ вместо того чтобы пересылать данные непосредственно между диском и
пользовательской памятью, системные вызовы read () и write () делают
две пересылки: между диском и буфером ядра и между буфером ядра и
пользовательской памятью.
Поскольку аппаратные блочные устройства должны управляться с помощью
прерываний и механизма DMA, а сделать это можно только в режиме ядра,
приложениям с самокэшированием определенно требуется некоторая под-
поддержка со стороны ядра.
Linux предлагает разработчикам простой способ обхода кэша страниц —
прямой ввод/вывод. При прямом вводе/выводе ядро программирует контрол-
контроллер диска так, чтобы он пересылал данные непосредственно на страницы
(или от них), принадлежащие адресному пространству режима пользователя,
которое выделено приложению с самокэшированием.
Как мы знаем, каждая пересылка данных протекает асинхронно. Когда она
выполняется, ядро может переключиться на другой процесс; центральный
процессор может возвратиться в режим пользователя; страницы процесса,
запустившего пересылку данных, могут быть выгружены и т. д. Все работает
хорошо при обычном вводе/выводе данных, поскольку в нем задействованы
страницы кэшей диска. Кэшами диска владеет ядро, они не могут быть вы-
выгружены, и они видны всем процессам, работающим в режиме ядра.
Зато при прямом вводе/выводе данные должны перемещаться в пределах
страниц, принадлежащих адресному пространству режима пользователя, ко-
которое представлено процессу. Ядро должно позаботиться о том, чтобы эти
страницы были доступны каждому процессу, работающему в режиме ядра, и
что они не будут выгружены, пока выполняется пересылка данных. Посмот-
Посмотрим, как это достигается.
Когда приложение с самокэшированием хочет обратиться к файлу напрямую,
оно открывает его с флагом odirect (см. разд. "Системный вызов openQ"
главы 12). При обслуживании системного вызова open о функция
dentryopen() проверяет, реализован ли метод directio для объекта
addressspace открываемого файла и возвращает код ошибки, если метода
нет. Флаг odirect может быть также установлен для файла, уже открытого с
использованием команды fsetfl системного вызова f cnti ().
Вначале рассмотрим случай, когда приложение с самокэшированием делает
системный вызов read () с флагом odirect. Как было сказано в разд. "Чтение
из файла" ранее в этой главе, метод read для файла обычно реализуется
функцией genericfiiereado, которая инициализирует дескрипторы iovec и
kiocb И ВЫЗЫВает функцию genericf ileaioread (). Последняя проверяет
корректность буфера режима пользователя, описываемого дескриптором
iovec, затем проверяет, установлен ли флаг odirect для файла. Вызванная
системным вызовом read (), эта функция выполняет фрагмент кода, эквива-
эквивалентный следующему:
if (filp->f_flags & O_DIRECT) {
if (count == 0 || *ppos > filp->f mapping->host->i size)
return 0;
retval = generic_file_direct_IO(READ, iocb, iov, *ppos, 1);
if (retval > 0)
*ppos += retval;
file_accessed(filp);
return retval;
}
Функция проверяет текущее положение файлового указателя, размер файла и
количество запрошенных символов, а затем вызывает функцию
generic fiiedirect loo, передавая ей тип операции read, дескриптор iocb,
дескриптор iovec, текущее значение файлового указателя и количество буфе-
буферов режима пользователя, заданных в дескрипторе iovec (один). Когда
функция generic_file_direct_IO() Завершает работу, функция generic_
fiieaioreado обновляет файловый указатель, ставит отметку времени на
индексном дескрипторе файла и возвращает управление.
Аналогичные действия происходят, когда системный вызов write () делается
для файла с установленным флагом odirect. Как было сказано в разд. "Запись
в файл"ранее в этой главе, метод write файла, в конечном счете, вызывает
функцию generic file aiowritenoiock. Она проверяет, установлен ли флаг
ODIRECT, И, еСЛИ установлен, вызывает функцию generic_file_
directio (), задавая write в качестве типа операции.
Функция genericfiiedirectioo принимает следующие параметры:
□ rw — тип операции, read или write;
□ iocb — указатель на дескриптор kiocb (см. табл. 16.1);
□ iov — указатель на массив дескрипторов iovec;
□ offset — смещение внутри файла;
С\ nrsegs — количество дескрипторов iovec в массиве iov.
Функция genericf iledirectIO () ВЫПОЛНЯет Следующие деЙСТВИЯ!
1. Получает адрес file файлового объекта из поля kifilp дескриптора kiocb
И адрес mapping объекта address_space ИЗ ПОЛЯ f ile->f_mapping.
2. Если задан тип операции write, и один или несколько процессов создали
отображение в память некоторой порции этого файла, функция вызывает
unmapmappingrange (), чтобы отменить отображение всех страниц файла.
Эта функция также гарантирует, что если какая-нибудь запись в Таблице
Страниц, у которой отменяется отображение, имеет установленный бит
Dirty, то эта страница будет помечена как грязная в кэше страниц.
3. ЕСЛИ индексное дерево С корнем В поле mapping не пусто (mapping->nrpages
больше Нуля), фунКЦИЯ ВЫЗЫВаеТ фуНКЦИИ filemap_fdatawrite() И
f ilemapf datawait (), чтобы принудительно записать на диск все грязные
страницы и подождать, пока операции записи не будут завершены (см.
разд. "Принудительная запись на диск грязных страниц отображения в
память"ранее в этой главе). Даже если приложение с самокэшированием
обращается к файлу напрямую, в системе могут быть другие приложения,
обращающиеся к нему через кэш страниц. Чтобы избежать потери данных,
образ диска синхронизируется с кэшем страниц до начала операции пря-
прямого ввода/вывода.
4. Вызывает метод directio адресного пространства mapping.
5. Если задан тип операции write, функция вызывает функцию
invaiidate_inode_pages2о, чтобы перебрать все страницы в индексном
дереве адресного пространства mapping и освободить их. Функция также
очищает записи в Таблице Страниц режима пользователя, которые ссы-
ссылаются на эти страницы.
В большинстве случаев метод directio является интерфейсом к функции
biockdev_direct_ioo. Эта функция очень сложна и использует большое
количество структур и функций. Впрочем, она выполняет операции, уже опи-
описанные в этой главе: разбирает читаемые или записываемые данные на под-
подходящие блоки, находит данные на диске и заполняет один или несколько
дескрипторов bio, описывающих операции ввода/вывода, которые необходи-
необходимо выполнить. Конечно, данные будут прочитаны или записаны прямо в бу-
буферы режима пользователя, заданные дескрипторами iovec из массива iov.
Дескрипторы bio передаются общему слою работы с блочными устройства-
устройствами, для чего вызывается функция submitbioo (см. разд. "Передача голов
буферов общему слою работы с блочными устройствами" главы 15). Как
правило, функция biockdev_direct_io () не возвращает управление, пока
все операции прямого ввода/вывода не будут завершены. Следовательно,
если системный вызов read () или write () возвратил управление, приложение
с самокэшированием может безбоязненно обращаться к буферам, содержа-
содержащим данные из файла.
Асинхронный ввод/вывод
Стандарт POSIX 1003.1 определяет набор библиотечных функций (перечис-
(перечисленных в табл. 16.4) для асинхронного обращения к файлам. Термин "асин-
хронное" означает, что, когда процесс, работающий в режиме пользователя
вызывает библиотечную функцию для чтения или записи в файл, эта функция
заканчивает работу сразу после того, как операция чтения или записи будет
поставлена в очередь, не дожидаясь, пока фактическая пересылка данных бу-
будет выполнена. Таким образом, вызвавший процесс сможет продолжить ра-
работу во время пересылки данных.
Таблица 16.4. Библиотечные функции РОБМдля асинхронного ввода/вывода
Функция Описание
aio_read () Асинхронно читает данные из файла
aio_write () Асинхронно записывает данные в файл
aio_f sync () Запрашивает операцию сброса на диск для всех ожидающих своего
выполнения операций асинхронного ввода/вывода (неблокирующий
вызов)
aio_error() Получает код ошибки для операции асинхронного ввода/вывода,
ожидающей выполнения
aio_return() Получает код возврата для выполненной операции асинхронного
ввода/вывода
aio_cancel () Отменяет операцию асинхронного ввода/вывода, ожидающую выпол-
выполнения
aiosuspend () Приостанавливает процесс, пока не завершится хотя бы одна из не-
нескольких операций асинхронного ввода/вывода, ожидающих выпол-
выполнения
Пользоваться асинхронным вводом/выводом очень просто. Приложение от-
открывает файл обычным системным вызовом ореп(). Затем оно заполняет
управляющий блок типа struct aiocb информацией, описывающей запро-
запрошенную операцию. В большинстве случаев используются следующие поля
управляющего блока struct aiocb:
□ aiofiides — дескриптор файла (возвращенный системным вызовом
open ());
□ aiobuf — буфер режима пользователя для данных файла;
□ aionbytes — количество байтов, подлежащих пересылке;
□ aioof f set — позиция в файле, с которой должна начаться операция чте-
чтения или записи (не связана с "синхронным" указателем файла).
Наконец, приложение передает адрес управляющего блока либо функции
aioread (), либо функции aiowrite (). Каждая из них завершит работу сразу
после того, как запрошенная операция пересылки данных будет поставлена в
очередь системной библиотекой или ядром. Впоследствии приложение смо-
жет проверить состояние операции ввода/вывода, вызвав функцию
aioerror о, которая возвращает einprogress, если пересылка данных все еще
продолжается, 0, если она успешно завершилась, и код ошибки, если про-
произошел сбой. Функция aioretum () возвращает количество байтов, фактиче-
фактически прочитанных или записанных операцией асинхронного ввода/вывода, или
-1 в случае сбоя.
Асинхронный ввод/вывод в Linux 2.6
Асинхронный ввод/вывод может быть реализован системной библиотекой
без какой бы то ни было поддержки со стороны ядра. Библиотечные функции
aioreado и aio_write() могут просто клонировать текущий процесс и по-
позволить ПрОЦеССу-ПОТОМКу ВЫПОЛНИТЬ СИНХрОННЫе СИСТеМНЫе ВЫЗОВЫ read ()
и write (). Затем процесс-родитель может завершить выполнение функции
aioread () или aiowrite () и продолжить выполнение программы, не дожи-
дожидаясь окончания синхронной операции, запущенной потомком. Однако такая
версия функций POSIX "для бедных" работает значительно медленнее, чем
версия, использующая реализацию асинхронного ввода/вывода на уровне
ядра.
Ядро Linux 2.6 имеет набор системных вызовов для асинхронного вво-
ввода/вывода. Однако в версии Linux 2.6.11 их разработка еще не закончена, и
асинхронный ввод/вывод работает корректно только для файлов, открытых с
установленным флагом odirect. Системные вызовы для асинхронного вво-
ввода/вывода перечислены в табл. 16.5.
Таблица 16.5. Системные вызовы Linux для асинхронного ввода/вывода
Системный вызов Описание
iosetup () Инициализирует асинхронный контекст для текущего процесса
io_submit () Передает одну или несколько операций асинхронного
ввода/вывода
iogetevents () Получает информацию о состоянии некоторых операций
асинхронного ввода/вывода, ожидающих своего выполнения
io_cancel () Отменяет операцию асинхронного ввода/вывода, ожидающую
выполнения
io_destroy () Удаляет асинхронный контекст текущего процесса
Контекст асинхронного ввода/вывода
Если процесс, работающий в режиме пользователя, хочет сделать системный
вызов iosubmit (), чтобы запустить операцию асинхронного ввода/вывода,
он должен будет заранее создать контекст асинхронного ввода/вывода.
В принципе, контекст асинхронного ввода/вывода является набором струк-
структур, которые отслеживают ход выполнения операций асинхронного вво-
ввода/вывода, запрошенных процессом. Каждый контекст асинхронного вво-
ввода/вывода ассоциируется с объектом kioctx, который хранит информацию,
имеющую отношение к контексту. Приложение может создать несколько
контекстов асинхронного ввода/вывода, и все дескрипторы kioctx собраны в
однонаправленный список с корнем в поле ioctxiist дескриптора памяти
(см. табл. 9.2).
Мы не будем подробно обсуждать объект kioctx, но остановимся на одной
важной структуре, на которую ссылается объект kioctx: на кольце асинхрон-
асинхронного ввода/вывода.
Кольцо асинхронного ввода/вывода— это буфер памяти в адресном про-
пространстве процесса, работающего в режиме пользователя, который доступен
также и всем процессам, работающим в режиме ядра. Начальный адрес в ре-
режиме пользователя и длина кольца асинхронного ввода/вывода хранятся, со-
соответственно, В ПОЛЯХ ring_info.ramap_base И ring_info.ramap_size объекта
kioctx. Дескрипторы всех страничных кадров, образующих кольцо асинхрон-
асинхронного ввода/вывода, хранятся в массиве, на который указывает поле
ring_infо.ring pages.
По своей сути, кольцо асинхронного ввода/вывода является циклическим бу-
буфером, в который ядро записывает отчеты о завершении операций асинхрон-
асинхронного ввода/вывода, ожидающих своего выполнения. В первых байтах кольца
асинхронного ввода/вывода находится заголовок (структура типа struct
aioring), а остальные байты содержат структуры ioevent, каждая из кото-
которых описывает завершенную операцию асинхронного ввода/вывода. По-
Поскольку страницы кольца асинхронного ввода/вывода отображаются в ад-
адресное пространство процесса режима пользователя, приложение может
непосредственно проверить ход выполнения операций асинхронного вво-
ввода/вывода, не прибегая к относительно медленному системному вызову.
Системный вызов iosetupO создает новый контекст асинхронного вво-
ввода/вывода для вызывающего процесса. Он принимает два параметра: макси-
максимальное количество ожидающих выполнения операций асинхронного вво-
ввода/вывода, которое, в конечном счете, определяет размер кольца асинхронно-
асинхронного ввода/вывода, и указатель на переменную, которая будет хранить
дескриптор контекста. Этот дескриптор одновременно является базовым ад-
адресом кольца асинхронного ввода/вывода. Служебная процедура sys_io_
setup () вызывает функцию dojnmap (), чтобы выделить новую анонимную об-
область памяти для процесса, которая будет содержать кольцо асинхронного
ввода/вывода (см. главу 9), а также создает и инициализирует объект kioctx,
описывающий контекст асинхронного ввода/вывода.
В противоположность этому, системный вызов iodestroy () удаляет контекст
асинхронного ввода/вывода. Он также уничтожает анонимную область памя-
памяти, содержащую кольцо асинхронного ввода/вывода. Системный вызов бло-
блокирует текущий процесс до тех пор, пока все операции асинхронного вво-
ввода/вывода не будут выполнены.
Передача операций асинхронного ввода/вывода
Для фактического запуска операций асинхронного ввода/вывода приложение
делает системный вызов iosubmit (), который принимает три параметра:
□ ctxid— ДеСКрИПТОр, ВОЗВращеННЫЙ СИСТеМНЫМ ВЫЗОВОМ io_setup() И
идентифицирующий контекст асинхронного ввода/вывода;
□ iocbpp — адрес массива указателей на дескрипторы типа iocb, каждый из
которых описывает одну операцию асинхронного ввода/вывода;
□ пг — длина массива, на который указывает параметр iocbpp.
Структура iocb состоит из тех же полей, что и POSIX-дескриптор aiocb
(aio_fildes, aio buf, aio_nbytes, aio offset) ПЛЮС поле aio lio opcode, KOTO-
poe содержит тип запрошенной операции (как правило, чтение, запись или
синхронизация).
Служебная процедура sysiosubmit () выполняет следующие действия:
1. Проверяет корректность массива дескрипторов iocb.
2. Ищет объект kioctx, соответствующий дескриптору ctxid, в списке, ко-
корень которого находится в поле ioctxiist дескриптора памяти.
3. Для каждого дескриптора iocb в массиве функция выполняет следующие
действия:
• получает адрес файлового объекта, соответствующего дескриптору
файла, хранящегося в поле aiof iides;
• выделяет и инициализирует новый дескриптор kiocb для операции вво-
ввода/вывода;
• проверяет наличие свободного слота в кольце асинхронного вво-
ввода/вывода для результата выполнения операции;
• задает метод kiretry дескриптора kiocb в соответствии с типом опера-
операции (см. ниже).
4. Выполняет функцию aioruniocb (), которую вызывает метод kiretry,
чтобы запустить фактическую пересылку данных для операции асинхрон-
асинхронного ввода/вывода. Если метод kiretry возвращает код -eiocbretry, зна-
значит, операция асинхронного ввода/вывода была передана на выполнение,
но еще не полностью удовлетворена: функция aioruniocb () будет вы-
вызвана снова для этого дескриптора kiocb, но немного позже (см. ниже).
В противном случае описываемая функция вызывает функцию
aiocompiete(), чтобы добавить сообщение о завершении операции в
кольцо контекста асинхронного ввода/вывода.
Если операция асинхронного ввода/вывода является запросом на чтение, ме-
метод kiretry соответствующего дескриптора kiocb реализуется функцией
aiopread (). Эта функция выполняет метод aioread файлового объекта, а
затем обновляет поля kibuf и kileft дескриптора kiocb (см. табл. 16.1 ранее
в этой главе) в соответствии со значением, которое возвратил метод aioread.
В конце работы функция aiopreado возвращает количество байтов, факти-
фактически прочитанных из файла, или значение -eiocbretry, если функция опре-
определила, что не все запрошенные байты были переданы. В большинстве фай-
файловых систем метод aioread файлового объекта сводится к вызову функции
generic_fiie_aio_read(). Предполагая, что флаг odirect у файла установ-
установлен, эта функция вызывает функцию genericfiiedirectioo, как было
описано в предыдущем разделе. Однако в этом случае функция
biockdev_direct_ioo не блокирует текущий процесс, ожидающий завер-
завершения пересылки данных, а возвращает управление немедленно. Поскольку
операция асинхронного ввода/вывода все еще ждет своего выполнения,
функция aioruniocb () будет вызвана снова, на этот раз потоком ядра aio из
рабочей очереди aiowq. Дескриптор kiocb следит за ходом выполнения вво-
ввода/вывода. В конце концов, все запрошенные данные будут пересланы, и ре-
результат выполнения будет занесен в кольцо асинхронного ввода/вывода.
Аналогичным образом, если операция асинхронного ввода/вывода является
запросом на запись, метод kiretry соответствующего дескриптора kiocb
реализуется функцией aio_pwrite () . Эта фуНКЦИЯ ВЫПОЛНЯет меТОД aio_write
файлового объекта, а затем обновляет поля kibuf и kileft дескриптора
kiocb (см. табл. 16.1 ранее в этой главе) в соответствии со значением, которое
возвратил метод aiowrite. В конце работы функция aiopwrite о возвращает
количество байтов, фактически записанных в файл, или значение -eiocbretry,
если функция определила, что не все запрошенные байты были переданы.
В большинстве файловых систем метод aiowrite файлового объекта сводит-
сводится к вызову функции genericfiieaiowritenoiocko . Предполагая, что флаг
odirect у файла установлен, эта функция вызывает функцию
genericf iledirectIO (), как было ОПИСанО ВЫШС
ГЛАВА 17
Утилизация страничных кадров
В предыдущих главах мы объяснили, как ядро управляет динамической па-
памятью, отслеживая свободные и занятые страничные кадры. Мы также гово-
говорили, что у каждого процесса, работающего в режиме пользователя, есть соб-
собственное адресное пространство, а ядро удовлетворяет запросы процесса на
память постранично, так что страничные кадры присваиваются процессу в
самый последний момент. Наконец, мы показали, как ядро пользуется дина-
динамической памятью для реализации кэшей памяти и диска.
В этой главе мы дополним наше описание подсистемы работы с виртуальной
памятью обсуждением утилизации страничных кадров. В первом разд. ''Алго-
''Алгоритм утилизации страничных кадров" мы объясним, почему ядру нужно
утилизировать страничные кадры и какой стратегии оно при этом придержи-
придерживается. Во втором разделе мы сделаем техническое отступление и опишем
структуры данных, используемые ядром для быстрого нахождения всех запи-
записей в Таблице Страниц, которые указывают на один и тот же страничный
кадр. Разд. "Реализация алгоритма PFRA" посвящен алгоритму утилизации
страничных кадров, используемому в Linux. Последний из разделов этого
уровня, "Подкачка", является как бы главой в главе. Он описывает подсисте-
подсистему подкачки — компонент ядра, используемый для сохранения на диске ано-
анонимных (не являющихся отображением файла) страниц.
Алгоритм утилизации страничных кадров
Один из удивительных свойств системы Linux является то, что проверки, вы-
выполняемые перед выделением динамической памяти ядру или процессам, ра-
работающим в режиме пользователя, довольно поверхностны.
Linux не делает никаких строгих проверок, например, по поводу количества
оперативной памяти, выделенной процессам, созданным одним пользовате-
пользователем (ограничения, упомянутые в главе 3, касаются, в основном, одиночных
процессов). Аналогичным образом, нет никакого ограничения на размер раз-
разнообразных кэшей диска и памяти, которыми пользуется ядро.
Такой слабый контроль является сознательным выбором разработчиков. Он
позволяет ядру использовать доступную оперативную память оптимальным
образом. Когда нагрузка на систему невелика, оперативная память занята, по
большей части, кэшами диска и немногими процессами, которые пользуются
информацией, хранящейся в кэшах. Однако, когда нагрузка возрастает, опе-
оперативная память заполняется страницами процессов, а кэши "ужимаются",
чтобы освободить место для новых процессов.
Как мы видели в предыдущих главах, кэши памяти и кэши диска забирают
себе все больше и больше страничных кадров, но никогда не освобождают
их. Это разумно, поскольку система кэширования не знает, будут ли процес-
процессы еще раз обращаться к кэшированным данным, и если будут, то когда. По-
Поэтому она не в состоянии определить, какие порции кэша могут быть осво-
освобождены. Кроме того, благодаря механизму выделения страниц по требова-
требованию, описанному в главе 9, процессы режима пользователя получают новые
страничные кадры по мере своего выполнения. Однако у этого механизма нет
способа заставить процессы освобождать страничные кадры, когда они
больше не нужны.
Таким образом, рано или поздно вся память оказывается выделенной процес-
процессам и кэшам. Алгоритм утилизации страничных кадров ядра Linux пополняет
списки свободных блоков системы, "воруя" страничные кадры как у процес-
процессов режима пользователя, так и у кэшей ядра.
На самом деле, утилизация страниц должна происходить до того, как будет
занята вся свободная память. В противном случае ядро может легко попасть в
западню из запросов на память, ведущую к краху системы. По идее, чтобы
освободить страничный кадр, ядро должно записать его содержимое на диск.
Однако, чтобы выполнить эту операцию, ядру потребуется еще один стра-
страничный кадр (например, для размещения голов буферов для пересылки дан-
данных). Если нет свободного страничного кадра, то никакой страничный кадр
освободить не удается.
Таким образом, одной из целей утилизации страничных кадров является ре-
резервирование минимального пула свободных страничных кадров, чтобы ядро
могло справиться с ситуациями нехватки памяти.
Выбор целевой страницы
Цель алгоритма утилизации страничных кадров, или алгоритм PFRA (Page
Frame Reclaiming Algorithm), состоит в том, чтобы выбирать страничные кад-
ры и освобождать их. Очевидно, страничные кадры, выбираемые алгоритмом
PFRA, должны быть несвободны, т. е. они не должны находиться ни в одном
из массивов f reearea, используемых buddy-системой (см. главу 8).
Алгоритм утилизации страничных кадров обращается со страничными кад-
кадрами по-разному, в зависимости от их содержимого. Мы будем различать не-
утилизируемые страницы, выгружаемые страницы, синхронизируемые стра-
страницы и страницы на выброс. Эти типы страниц представлены в табл. 17.1.
Таблица 17.1. Типы страниц в алгоритме PFRA
Тип страниц °™са" ?о^иВлиЯзации
Неутилизируемые Свободные страницы (включенные в списки Утилизация
buddy-системы) не разрешена или
не требуется
Зарезервированные страницы (с установ-
установленным флагом PG_reserved)
Страницы, динамически выделенные ядром
Страницы в стеках режима ядра процессов
Временно заблокированные страницы
(с установленным флагом PG_locked)
Страницы, заблокированные в памяти
(в областях памяти, для которых установ-
установлен флаг vm_locked)
Выгружаемые Анонимные страницы в адресных простран- Сохранить содержи-
ствах режима пользователя мое страницы
л _ _,_ w в область подкачки
Отображающие страницы файловой систе-
системы tmpfs (например, страницы совместно
используемой памяти межпроцессного
взаимодействия)
Синхронизируемые Отображающие страницы в адресных про- Синхронизировать
странствах режима пользователя страницу с ее обра-
л зом на диске, если
Страницы, включенные в кэш страниц необходимо
и содержащие данные из файлов на диске
Страницы буферов блочных устройств
Страницы некоторых кэшей диска (напри-
(например, кэша индексных дескрипторов)
На выброс Неиспользуемые страницы в кэшах памяти Ничего не делать
(например, в кэшах slab-аллокатора)
Неиспользуемые страницы в кэше элемен-
элементов каталога
В этой таблице страница называется отображающей, если она отображает
фрагмент файла. Например, все страницы в адресных пространствах режима
пользователя, принадлежащие отображениям файлов в память, являются ото-
отображающими, как и любые другие страницы, содержащиеся в кэше страниц.
Почти во всех случаях отображающие страницы являются синхронизируе-
синхронизируемыми: чтобы утилизировать страничный кадр, ядро должно проверить, явля-
является ли страница "грязной", и, если необходимо, записать ее содержимое в
соответствующий файл на диске.
В отличие от таких страниц, страница называется анонимной, если она при-
принадлежит анонимной области памяти процесса (например, все страницы про-
процесса в куче или стеке режима пользователя анонимны). Чтобы утилизиро-
утилизировать страничный кадр, ядро должно сохранить содержимое страницы в спе-
специально отведенном разделе диска, называемом областью подкачки (см.
разд. "Подкачка11 далее в этой главе). Следовательно, все анонимные страни-
страницы являются выгружаемыми.
Как правило, страницы специальных файловых систем не являются утилизи-
утилизируемыми. Единственным исключением являются страницы специальной фай-
файловой системы tmpfs, которые могут быть утилизированы после сохранения
их в области подкачки. Как мы увидим в главе 19, специальная файловая сис-
система tmpfs требуется механизму совместного использования памяти при
межпроцессном взаимодействии.
Когда алгоритм утилизации страничных кадров должен затребовать странич-
страничный кадр, принадлежащий адресному пространству процесса режима поль-
зовтеля, он вначале определяет, является ли страничный кадр совместно
используемым или нет. Совместно используемый страничный кадр принад-
принадлежит нескольким адресным пространствам режима пользователя, а странич-
страничный кадр, таковым не являющийся, — только одному. Обратите внимание,
что страничный кадр, не используемый совместно, может принадлежать не-
нескольким "облегченным" процессам, ссылающимся на один дескриптор па-
памяти.
Совместно используемые страничные кадры обычно создаются, когда про-
процесс порождает потомка. Как было сказано в главе 9, таблицы страниц по-
потомка копируются с таблиц породившего процесса, поэтому родитель и по-
потомок совместно используют одни и те же страничные кадры. Другим типич-
типичным случаем является обращение двух или более процессов к одному файлу
при посредстве совместно используемого отображения в память (см.
разд. "Отображение в память"главы 16)х.
1 Впрочем, следует заметить, что, когда один процесс обращается к файлу через совместно исполь-
используемое отображение, соответствующие страницы не являются совместно используемыми, с точки
Описание алгоритма PFRA
В то время как выбрать страницы-кандидаты на утилизацию (грубо говоря —
это все страницы, принадлежащие кэшу диска или памяти или адресному
пространству процесса, работающего в режиме пользователя) довольно лег-
легко, выбор правильных целевых страниц является, пожалуй, самой тонкой за-
задачей при разработке ядра.
Для разработчика, создающего подсистему виртуальной памяти, самым труд-
трудным является поиск алгоритма, который обеспечил бы приемлемую произво-
производительность как для настольных компьютеров (на которых требования к па-
памяти не высоки, а время отклика системы критично), так и для высокопроиз-
высокопроизводительных компьютеров, например, серверов больших баз данных (где
запросы на память могут быть огромны).
К сожалению, поиск хорошего алгоритма утилизации страничных кадров но-
носит, в основном, эмпирический характер и имеет слабые теоретические обос-
обоснования. Ситуация в какой-то степени аналогична оценке факторов, опреде-
определяющих динамический приоритет процесса: главная цель — подобрать пара-
параметры так, чтобы была достигнута высокая производительность системы, и
не искать объяснений, почему все работает хорошо. Нередко разработчики
руководствуются принципом "давайте попробуем и посмотрим, что получит-
получится". Неприятным побочным эффектом подобного эмпирического проектиро-
проектирования является быстрое изменение кода. Учитывая вышесказанное, мы не
можем гарантировать, что алгоритм утилизации памяти, который мы собира-
собираемся описать (и который применяется в Linux 2.6.11), будет официально при-
принятым алгоритмом в последней версии ядра Linux 2.6 в момент, когда вы бу-
будете читать эту книгу. Впрочем, общие идеи и основные эвристические пра-
правила, описанные здесь, должны остаться неизмененными.
Бывает, что за деревьями не видно леса. Поэтому мы вначале представим не-
несколько общих правил, принятых в алгоритме PFRA. Эти правила встроены в
функции, которые будут описаны далее в этой главе.
□ В первую очередь освобождать "безвредные" страницы— страницы кэ-
кэшей диска и памяти, на которые не ссылается ни один процесс, должны
утилизироваться раньше страниц, принадлежащих адресным пространст-
пространствам процессов режима пользователя, ведь в первом случае утилизация
страничного кадра может быть выполнена без модификации записи в Таб-
Таблице Страниц. Как мы увидим в разд. "Списки давно неиспользуемых
зрения алгоритма утилизации страничных кадров. Аналогичным образом страница, принадлежащая
закрытому отображению в память, может быть воспринята алгоритмом утилизации страничных
кадров как совместно используемая (например, если два процесса читают одну порцию файла, но ни
один из них не модифицирует данные на странице).
страниц (LRU)" далее в этой главе, это правило до некоторой степени
смягчается введением "фактора тенденции к выгрузке".
□ Сделать все страницы процесса в режиме пользователя утилизируемы-
утилизируемыми — если не принимать во внимание заблокированные страницы, алго-
алгоритм PFRA должен быть в состоянии "украсть" любую страницу процесса,
работающего в режиме пользователя. Таким образом, процессы, которые
"спят" слишком долго, постепенно растеряют все свои страничные кадры.
□ Утилизировать совместно используемый страничный кадр за счет одно-
одновременного стирания всех записей в таблице страниц, ссылающихся на не-
него — когда алгоритм PFRA пытается освободить страничный кадр, совме-
совместно используемый несколькими процессами, он стирает все записи в таб-
таблице страниц, ссылающиеся на этот кадр, и только потом утилизирует его.
□ Утилизировать только "неиспользуемые" страницы— алгоритм PFRA
применяет упрощенную версию алгоритма замещения давно не исполь-
используемых элементов, когда делит страницы на используемые и неиспользуе-
неиспользуемые2. Если к странице продолжительное время не обращались, то вероят-
вероятность, что процесс обратится к ней в ближайшем будущем, мала, и стра-
страницу можно считать неиспользуемой. С другой стороны, если последнее
обращение к странице произошло недавно, то вероятность повторного об-
обращения велика, и страницу следует считать используемой. Алгоритм
PFRA утилизирует только неиспользуемые страницы. Это еще одно про-
проявление принципа локальности, упомянутого в главе 2.
Главная идея, положенная в основу алгоритма "давно не используемых
элементов" состоит в том, чтобы связать с каждой страницей в оператив-
оперативной памяти счетчик возраста, т. е. интервал времени с последнего обраще-
обращения к этой странице. Такой счетчик позволит алгоритму PFRA утилизиро-
утилизировать только самые старые страницы любого процесса. Некоторые компью-
компьютерные платформы предоставляют сложную поддержку алгоритмам
"давно не используемых элементов. К сожалению, процессоры 80x86
такой аппаратной возможности не имеют, и ядро Linux не может пола-
полагаться на счетчик, отсчитывающий возраст каждой страницы. Чтобы вый-
выйти из положения, Linux использует бит Accessed, имеющийся у каждой
записи Таблицы Страниц. Этот бит автоматически устанавливается аппа-
аппаратной частью при обращении к странице. Кроме того, возраст страницы
2 Алгоритм PFRA мог бы обратиться и к алгоритму "использованный однажды", ведущему свое
происхождение от алгоритма замещения буфера 2Q, предложенного Т. Джонсоном (Т. Johnson) и
Д. Шашей (D. Shasha) в 1994 г.
3 Например, центральные процессоры некоторых суперкомпьютеров автоматически обновляют
значения счетчика, имеющего в каждой записи таблицы страниц и показывающего возраст соответ-
соответствующей страницы.
представлен позицией ее дескриптора в одном из двух списков (см.
разд. "Списки давно неиспользуемых страниц (LRU) " далее в этой главе).
Итак, алгоритм утилизации страничных кадров является комбинацией не-
нескольких эвристик:
□ тщательного определения порядка, в котором проверяются кэши;
□ упорядочения страниц по возрасту (давно не используемые страницы
должны освобождаться раньше страниц, использованных недавно);
□ разграничения страниц по состоянию (например, "не грязные" страницы
являются более подходящими кандидатами, чем "грязные", поскольку их
не нужно записывать на диск).
Обратное отображение
Как было сказано в предыдущем разделе, одной из целей алгоритма утилиза-
утилизации страничных кадров является освобождение совместно используемого
страничного кадра. Поэтому у ядра Linux 2.6 есть возможность быстрого по-
поиска всех записей в Таблице Страниц, указывающих на один и тот же стра-
страничный кадр. Такая функциональность называется обратным отображе-
отображением.
Тривиальным решением задачи обратного отображения было бы включение в
каждый дескриптор страницы дополнительных полей, которые связали бы
воедино все записи Таблицы Страниц, указывающие на страничный кадр,
ассоциированный с дескриптором страницы. Однако сопровождение таких
списков значительно увеличило бы накладные расходы ядра, и поэтому были
предложены более сложные решения. Методика, применяемая в Linux 2.6,
называется объектно-базированным обратным отображением. Его суть в.
том, что для любой утилизируемой страницы режима пользователя ядро хра-
хранит обратные ссылки на все области памяти в системе ("объекты"), которые
включают в себя эту страницу. Каждый дескриптор области памяти хранит
указатель на дескриптор памяти, который, в свою очередь, хранит указатель
на Глобальный Каталог Страниц. Следовательно, обратные ссылки позволя-
позволяют алгоритму утилизации страничных кадров получить все записи Таблицы
Страниц, которые ссылаются на данную страницу. Поскольку дескрипторов
областей памяти меньше, чем дескрипторов страниц, обновление обратных
ссылок на совместно используемые страницы отнимает меньше времени. По-
Посмотрим, как эта схема реализована на практике.
Во-первых, у алгоритма утилизации страничных кадров должен быть способ
определять, является ли страница, которую он собирается утилизировать, со-
совместно используемой, и является ли она отображающей или анонимной. Для
ЭТОГО ЯДрО Изучает Два ПОЛЯ ДеСКрИПТОра СТраНИЦЫ _mapcount И mapping.
В поле mapcount хранится количество записей Таблицы Страниц, ссылаю-
ссылающихся на данный страничный кадр. Счетчик начинает отсчет с -1, это значе-
значение свидетельствует о том, что на страничный кадр не ссылается ни одна
запись Таблицы Страниц. Таким образом, если счетчик равен нулю, страница
не является совместно используемой, а при положительном значении счетчи-
счетчика она таковой является. Функция pagemapcount () принимает адрес дескрип-
дескриптора страницы и возвращает значение его счетчика mapcount, увеличенное на
единицу (так, например, она возвращает 1 для страницы, не используемой
совместно и включенной в адресное пространство какого-то процесса режима
пользователя).
Поле mapping дескриптора страницы определяет, является ли страница ото-
отображающей или анонимной:
□ если поле mapping равно null, значит, страница принадлежит кэшу подкач-
подкачки (см. разд. "Кэш подкачки" далее в этой главе);
П если поле mapping не равно null, а его младший бит равен 1, значит, стра-
страница является анонимной, и поле mapping содержит указатель на дескрип-
дескриптор anonvma (см. следующий разд. "Обратное отображение для аноним-
анонимных страниц");
□ если поле mapping не равно null, а его младший бит равен 0, значит, стра-
страница является отображающей. Поле mapping указывает на объект
addressspace Соответствующего файла (см. разд. "Объект address_space"
в главе 15).
Каждый объект addressspace в Linux выровнен в оперативной памяти так,
что его начальный линейный адрес кратен четырем. Поэтому младший бит
поля mapping может быть использован как флаг, показывающий, содержит ли
поле указатель на объект address space или на дескриптор anonvma. Это пло-
плохой стиль программирования, но ядро работает с огромным количеством де-
дескрипторов, и эти структуры данных должны иметь как можно меньшие раз-
размеры. Функция PageAnonO принимает в качестве параметра адрес дескрипто-
дескриптора страницы. Она возвращает 1, если младший бит поля mapping установлен, и
О в противном случае.
Функция trytounmap (), принимающая в качестве параметра указатель на
дескриптор страницы, пытается очистить все записи в Таблице Страниц, ука-
указывающие на страничный кадр, ассоциированный с этим дескриптором.
Функция возвращает константу swapsuccess (ноль), если ей удалось удалить
ссылки на страничный кадр из всех записей Таблицы Страниц; константу
swapagain (единицу), если какие-то ссылки удалить не удалось; и константу
swapfail (двойку), если возникла ошибка.
Функция довольно короткая:
int try_to_unmap(struct page *page)
{
int ret;
if (PageAnon(page))
ret = try_to_unmap_anon(page);
else
ret = try_to_unmap_file(page);
if (!pagejnapped(page))
ret = SWAP_SUCCESS;
return ret;
}
Функции try_to_unmap_anon () И try_to_unmap_file () занимаются анонимны-
ми и отображающими страницами соответственно. Они будут описаны в по-
последующих разделах.
Обратное отображение для анонимных страниц
Анонимные страницы нередко используются несколькими процессами. Са-
Самый распространенный случай— создание нового процесса с помощью
fork (). Как было сказано в главе 9, все страничные кадры, принадлежащие
процессу-родителю (включая анонимные страницы), назначаются и потомку.
Другой (довольно редкий) случай имеет место, когда процесс создает область
памяти, устанавливая как флаг map_anonymous, так и флаг mapshared. Страни-
Страницы такой области будут использоваться совместно потомками этого процесса.
Стратегия, направленная на то, чтобы связать воедино все анонимные стра-
страницы, ссылающиеся на один страничный кадр, проста: анонимные области
памяти, содержащие этот страничный кадр, собираются в двунаправленный
циклический список. Необходимо отдавать себе отчет в том, что, даже если
анонимная область памяти содержит разные страницы, всегда существует
только один список обратного отображения для всех страничных кадров в
этой области.
Когда ядро присваивает первый страничный кадр анонимной области, оно
создает новую структуру anonvma, состоящую всего из двух полей: lock,
спин-бллокировки для защиты списка от параллельного обращения, и head,
головы двунаправленного циклического списка дескрипторов областей памя-
памяти. Затем ядро заносит дескриптор vmareastruct анонимной области памяти
в список структуры anonvma. Для этой цели структура vmareastruct имеет
два поля: anonvmanode содержит указатели на следующий и предыдущий
элементы списка, a anonvma указывает на структуру anonvma. Наконец, ядро
сохраняет адрес структуры anonvma в поле mapping дескриптора анонимной
страницы, как было описано ранее (рис. 17.1).
Рис. 17.1. Объектно-базированное обратное отображение для анонимных страниц
Когда страничный кадр, на который уже ссылается один процесс, заносится в
запись Таблицы Страниц, соответствующую другому процессу (например,
вследствие системного вызова fork), ядро просто вставляет анонимную об-
область памяти второго процесса в двунаправленный циклический список
структуры anonvma, на которую указывает поле anonvma области памяти,
принадлежащей первому процессу. Таким образом, список структуры
anonvma, как правило, содержит области памяти, которыми владеют разные
процессы4.
Как видно из рис. 17.1, список структуры anonvma позволяет ядру быстро
находить все записи Таблицы Страниц, ссылающиеся на один и тот же ано-
анонимный страничный кадр. Любой дескриптор области памяти хранит в поле
Список структуры anon_vma может также включать в себя несколько смежных анонимных об-
областей памяти, которыми владеет один процесс. Обычно это имеет место, когда анонимная область
памяти разбита на несколько областей системным вызовом mprotect ().
vmram адрес дескриптора памяти, который, в свою очередь, имеет поле pgd,
содержащее адрес Глобального каталога страниц данного процесса.
Тогда запись в Таблице Страниц может быть определена по начальному ли-
линейному адресу анонимной страницы, который легко находится по дескрип-
дескриптору области памяти и полю index дескриптора страницы.
Функция try_to_unmap_anon()
При утилизации анонимного страничного кадра алгоритм PFRA должен пе-
перебирать все области памяти в списке структуры anonvma и тщательно про-
проверять, содержит ли очередная область анонимную страницу, чей странич-
страничный кадр является целевым.
Эта задача решается с помощью функции trytounmapanono, которая при-
принимает в качестве параметра дескриптор целевого страничного кадра и вы-
выполняет следующие действия:
1. Получает спин-блокировку lock структуры anonvma, на которую указыва-
указывает поле mapping дескриптора страницы.
2. Сканирует список дескрипторов областей памяти, принадлежащий струк-
структуре anonvma. Для каждого дескриптора области памяти vma, найденного в
этом списке, функция вызывает функцию trytounmaponeO, передавая
ей в качестве параметров сам vma и дескриптор страницы. Если по какой-
либо причине вызванная функция возвратит значение swapfail, или поле
mapcount дескриптора страницы будет показывать, что все записи Табли-
Таблицы Страниц, ссылающиеся на данный страничный кадр, были найдены,
сканирование прекратится до достижения конца списка.
3. Освобождает спин-блокировку, полученную на шаге 1.
4. Возвращает значение, вычисленное при последнем вызове функции
try_to_unmap_one (), Т. е. SWAP_AGAIN (чаСТИЧНЫЙ успех) ИЛИ SWAP_FAIL (не-
удача).
Функция try_to__unmap_one()
Функция trytounmaponeO вызывается многократно, как из функции
try_to_unmap_anon(), так И ИЗ фуНКЦИИ try_to_unmap_file (). Она Принимает
два параметра: указатель page на дескриптор целевой страницы и указатель
vma на дескриптор области памяти. Эта функция выполняет следующие дей-
действия:
1. Вычисляет линейный адрес страницы, подлежащей утилизации, по на-
начальному линейному адресу области памяти (vma->vm_start), смещению
области памяти в отображенном файле (vma->vm_pgoff) и смещению стра-
ницы внутри отображенного файла (page->index). Для анонимных страниц
ПОЛе vma->vm_pgof f равно либо Нулю, либо vmstart/PAGESIZE. CoOTBeTCT-
венно, поле page->index равно либо индексу страницы внутри области па-
памяти, либо линейному адресу страницы, поделенному на pagesize.
2. Если целевая страница анонимна, функция проверяет, попадает ли ее ли-
линейный адрес внутрь области памяти. Если нет, функция завершает рабо-
работу, возвращая swapagain. (Как было сказано при описании обратного ото-
отображения для анонимных страниц, список структуры anonvma может
включать в себя области памяти, не содержащие целевую страницу.)
3. Извлекает адрес дескриптора памяти из поля vma->vm_mm и получает спин-
блокировку vma->vrn_mm->page_tabie_iock, который защищает таблицы
страниц.
4. Вызывает Друг За ДруГОМ фуНКЦИИ pgd_offset(), pud_offset(),
pmd_of f set () И pteof f setmap (), чтобы ПОЛучИТЬ адрес Записи Таблицы
Страниц, соответствующий линейному адресу целевой страницы.
5. Выполняет некоторые проверки, чтобы убедиться, что целевая страница
действительно является утилизируемой. Если хотя бы одна из следующих
проверок даст отрицательный результат, функция перейдет к шагу 12,
чтобы завершить работу и возвратить код ошибки, либо swapagain, либо
swap_fail:
• проверяет, что запись в Таблице Страниц действительно указывает на
целевую страницу. Если это не так, функция возвращает swapagain.
Это может произойти в следующих случаях:
D запись в Таблице Страниц ссылается на страничный кадр, назначен-
назначенный с помощью "копирования при записи", но анонимная область
памяти, идентифицируемая параметром vma, по-прежнему принад-
принадлежит списку anonvma исходного страничного кадра;
D системный вызов mremap () может переотобразить области памяти и
перенести страницы в адресное пространство режима пользователя,
редактируя записи в таблице страниц напрямую. В этом случае объ-
объектно-базированное обратное отображение не работает, потому что
поле index дескриптора страницы не может быть использовано для
определения фактического линейного адреса страницы;
п отображение файла в память является нелинейным (см. разд. "Не-
"Нелинейные отображения в память"главы 16);
• проверяет, является ли область памяти заблокированной (vmlocked)
или зарезервированной (vmreserved). Если одно из этих условий удов-
удовлетворено, функция возвращает значение swapfail;
• проверяет, сброшен ли бит Accessed в записи Таблицы Страниц. Если
нет, функция сбрасывает его и возвращает swapfail. Если бит
Accessed установлен, страница считается "используемой", и ее нельзя
утилизировать;
• проверяет, принадлежит ли страница кэшу подкачки (см. разд. "Кэш
подкачки" далее в этой главе), и обрабатывается ли она в настоящий
момент функцией getuserpages о (см. главу 9). В этом случае, чтобы
избежать неприятной конкуренции между процессами, функция воз-
возвращает SWAP_FAIL.
6. Итак, страницу можно утилизировать. Если бит Dirty в записи Таблицы
Страниц установлен, функция устанавливает флаг PGdirty для страницы.
7. Очищает запись Таблицы Страниц и соответствующие TLB-буферы.
8. Если страница является анонимной, функция заносит идентификатор вы-
выгружаемой страницы в запись Таблицы Страниц, чтобы при последую-
последующих обращениях к этой странице она была загружена обратно (см.
разд. "Подкачка" далее в этой главе). Кроме того, функция уменьшает
счетчик анонимных страниц, хранящийся в поле anonrss дескриптора
памяти.
9. Уменьшает счетчик страничных кадров, выделенных процессу, храня-
хранящийся в поле rss дескриптора памяти.
10. Уменьшает значение поля mapcount дескриптора страницы, потому что
ссылка на этот страничный кадр была удалена из записей Таблицы Стра-
Страниц режима пользователя.
И. Уменьшает счетчик обращений страничного кадра, хранящийся в поле
count дескриптора страницы. Если значение счетчика станет отрица-
отрицательным, функция удаляет дескриптор страницы из активного или неак-
неактивного списка (см. разд. "Списки давно неиспользуемых страниц (LRU)"
далее в этой главе) и вызывает функцию f reehotpage (), чтобы освобо-
освободить страничный кадр (см. главу 8).
12. Вызывает функцию pteunmapO, чтобы освободить временное отображе-
отображение в адресное пространство ядра, которое могло быть выделено функци-
функцией pte_of f set_map () на шаге 4 (см. главу 8).
13. Освобождает СПИН-блОКИроВКу vma->vmjran->page_table_lock, полученную
на шаге 3.
14. Возвращает соответствующий код (swapagain в случае успеха).
Обратное отображение
для отображающих страниц
Как и в случае с анонимными страницами, объектно-базированное обратное
отображение для отображающих страниц основано на простой идее: всегда
существует возможность получить записи Таблицы Страниц, ссылающиеся
на данный страничный кадр, путем обращения к дескрипторам областей па-
памяти, содержащих соответствующие отображающие страницы. Таким обра-
образом, суть обратного отображения заключается в хорошо продуманной струк-
структуре данных, которая собирает вместе все дескрипторы областей памяти, от-
относящиеся к данному страничному кадру.
В предыдущем разделе мы видели, что дескрипторы для анонимных областей
памяти собраны в двунаправленные циклические списки. Получение всех
записей из таблиц страниц, ссылающихся на данный страничный кадр, вклю-
включает в себя линейный перебор элементов списка. Количество совместно ис-
используемых анонимных страничных кадров никогда не бывает очень велико,
и такой подход хорошо работает.
В отличие от анонимных страниц, отображающие страницы часто являются
совместно используемыми, потому что разные процессы могут обращаться к
одним и тем же страницам кода. Например, может случиться, что почти все
процессы в системе совместно используют страницы, содержащие код стан-
стандартной библиотеки С (см. разд. "Библиотеки" главы 20). По этой причине в
Linux 2.6 применяются специальные поисковые деревья, называемые деревь-
деревьями приоритетного поиска, которые позволяют быстро обнаружить все об-
области памяти, ссылающиеся на один и тот же страничный кадр.
Дерево приоритетного поиска существует для каждого файла, а его корень
хранится В ПОЛе i_mmap объекта address_space, встроенного В объект inode
данного файла. Всегда есть возможность быстро получить корень этого поис-
поискового дерева, потому что поле mapping в дескрипторе отображающей стра-
страницы указывает на объект addressspace.
Дерево приоритетного поиска
Дерево приоритетного поиска, применяемое в Linux 2.6, основано на струк-
структуре данных, впервые предложенной Эдвардом Маккрейтом (Edward
McCreight) в 1985 г. для представления множества перекрывающихся интер-
интервалов. Дерево Маккрейта является гибридом кучи и сбалансированного дере-
дерева поиска, используется оно для выполнения запросов на множестве интерва-
интервалов (например, "какие интервалы содержатся в данном") за время, прямо
пропорциональное высоте дерева и количеству интервалов в ответе.
Каждый интервал в дереве приоритетного поиска соответствует узлу и харак-
характеризуется двумя индексами: базисным индексом, который соответствует на-
чальной точке интервала, и индексом кучи, который соответствует конечной
точке. Дерево приоритетного поиска фактически является деревом поиска по
базисному индексу с дополнительным кучевым свойством, заключающимся
в том, что индекс кучи узла не может быть меньше индексов кучи его по-
потомков.
Дерево приоритетного поиска Linux отличается от структуры Маккрейта в
двух важных аспектах. Во-первых, дерево Linux не всегда сбалансировано
(алгоритм балансирования дорог, как в отношении памяти, так и времени).
Во-вторых, дерево Linux адаптировано под хранение областей памяти, а не
интервалов.
Каждую область памяти можно рассматривать как интервал страниц файла,
идентифицируемый начальной позицией в файле (базисный индекс) и конеч-
конечной позицией (индекс кучи). Однако области памяти обычно начинаются с
одних и тех же страниц (как правило, со страницы с индексом 0), а ориги-
оригинальное дерево Маккрейта, к сожалению, не может хранить интервалы,
имеющие одинаковые начальные точки. В качестве половинчатого решения
каждый узел дерева приоритетного поиска Linux был снабжен дополнитель-
дополнительным индексом размера (отличным от базисного индекса и индекса кучи), ко-
который соответствует размеру области памяти в страницах минус один. Ин-
Индекс размера позволяет программе, выполняющей поиск, различать области
памяти, начинающиеся с одной позиции в файле.
Однако индекс размера значительно увеличивает потенциальное количество
разных узлов в дереве приоритетного поиска. В частности, если существует
слишком много узлов с одинаковым базисным индексом, но разными индек-
индексами кучи, дерево приоритетного поиска не сможет вместить их все. Чтобы
решить эту проблему, дерево может дополнительно включать в себя под-
поддеревья переполнения, имеющие корни в листьях дерева приоритетного поис-
поиска и содержащие узлы, имеющие общее базисное дерево.
Кроме того, разные процессы могут владеть областями памяти, отображаю-
отображающими в точности одну и ту же порцию какого-то файла (вспомним пример со
стандартной библиотекой С, приведенный ранее). В таком случае все узлы,
соответствующие этим областям памяти, имеют одинаковые базисные индек-
индексы, а также индексы кучи и размера. Когда ядру нужно вставить в дерево
приоритетного поиска какую-либо область памяти, индексы которой совпа-
совпадают с индексами уже существующего узла, оно заносит дескриптор этой об-
области памяти в двунаправленный циклический список с корнем в старом узле
дерева приоритетного поиска.
На рис. 17.2 представлен простой пример дерева приоритетного поиска. Сле-
Слева на рисунке показаны семь областей памяти, покрывающие первые шесть
страниц файла, и для каждого интервала приведены базисный индекс, индекс
размера и индекс кучи. Справа нарисовано соответствующее дерево приори-
приоритетного поиска. Обратите внимание, что ни один потомок не имеет индекс
кучи, превышающий индекс кучи его родителя. Кроме того, заметьте, что
базисный индекс левого потомка любого узла никогда не превышает базис-
базисный индекс правого потомка; в случае равенства базисных индексов узлы
упорядочиваются по индексу размера. Предположим, что алгоритм утилиза-
утилизации страничных кадров должен получить все области памяти, содержащие
страницу с индексом 5. Алгоритм поиска начинает работу с корня @,5,5): по-
поскольку соответствующий интервал включает эту страницу, получена первая
область памяти. Затем алгоритм переходит к левому потомку @,4,4) корня и
сравнивает индекс кучи D) с индексом страницы: поскольку индекс кучи
меньше, интервал не содержит эту страницу; более того, благодаря кучевому
свойству дерева приоритетного поиска, ни один потомок этого узла не может
содержать данную страницу. Тогда алгоритм переходит к правому потомку
B,3,5) корня. Соответствующий интервал содержит страницу, и, следова-
следовательно, получена вторая область памяти. Затем алгоритм посещает узлы-
потомки A,2,3) и B,0,2), но обнаруживает, что ни один из них не содержит
страницу.
Рис. 17.2. Простой пример дерева приоритетного поиска
Из-за недостатка места мы не сможем подробно описать структуры и функ-
функции, реализующие деревья приоритетного поиска Linux. Мы лишь упомянем,
что узел дерева приоритетного поиска представляется структурой
priotreenode, КОТОрая встроена В ПОЛе shared, priotreenode КажДОГО деСК-
риптора области памяти. Структура данных shared, vmset применяется (как
альтернатива структуре shared, priotreenode) для занесения дескриптора
области памяти в дублирующий список узла дерева приоритетного поиска.
Узлы этого дерева могут быть вставлены или удалены с помощью функций
vma_prio_tree_insert () И vma_prio_tree_rernove (). Обе ОНИ Принимают В ка-
честве параметров адрес дескриптора области памяти и адрес корня дерева
приоритетного поиска. Запросы к дереву выполняются с помощью макроса
vma priotreeforeach, который реализует цикл по всем дескрипторам облас-
областей памяти, содержащим хотя бы одну страницу в указанном диапазоне ли-
линейных адресов.
Функция try_to__unmap_file()
Функция trytounmapfileo вызывается функцией trytounmap () для вы-
выполнения обратного отображения отображающих страниц. Эта функция до-
довольно проста, когда отображение в память линейно (см. разд. "Ото-
"Отображение в память" главы 16). В этом случае она выполняет следующие
действия:
1. Получает СПИН-блОКИрОВКу page->mapping->i_rnmap_lock.
2. Применяет макрос vma_prio_tree_foreach () к дереву приоритетного поис-
поиска, корень КОТОРОГО хранится В ПОЛе page->mapping->i_mmap. Для каждого
дескриптора vmareastruct, найденного макросом, функция вызывает
функцию try_to_unmap_one(), чтобы попытаться очистить запись в Табли-
Таблице Страниц, относящуюся к области памяти, которая содержит данную
страницу (см. разд. "Обратное отображение для анонимных страниц"
ранее в этой главе). Если по какой-либо причине эта функция возвратит
значение swapfail, или если поле mapcount дескриптора страницы свиде-
свидетельствует о том, что все записи в Таблице Страниц, ссылающиеся на дан-
данный страничный кадр, были найдены, то сканирование немедленно пре-
прекращается.
3. Освобождает СПИН-блОКИрОВКу page->mapping->i_mmap_lock.
4. Возвращает swapagain или swapfail, в зависимости от того, все ли записи
таблицы страниц были очищены.
Если отображение в память нелинейно (см. разд. "Нелинейные отображения
в память" главы 16), у функции trytounmapone () может не получиться
очистить некоторые записи Таблицы Страниц, потому что поле index деск-
дескриптора страницы, которое, как обычно, содержит позицию страницы внутри
файла, не имеет отношения к позиции страницы в области памяти. В резуль-
результате функция trytounmapone() не сможет определить линейный адрес
страницы и, следовательно, не сможет получить адрес записи в Таблице
Страниц.
Единственным решением будет исчерпывающий поиск во всех нелинейных
областях памяти, выделенных для данного файла. Двунаправленный список с
корнем В ПОЛе i_mmap_nonlinear объекта addressspace, принадлежащего фай-
файлу page->mapping, содержит дескрипторы всех нелинейных областей памяти
этого файла. Для каждой такой области памяти функция trytounmapfile о
вызывает функцию trytounmapciustero, которая сканирует все записи
в Таблице Страниц, соответствующие линейным адресам в области памяти и
пытается очистить эти записи.
Поскольку подобный поиск может занять очень много времени, выполняется
ограниченное сканирование, а участок области памяти, который следует ска-
сканировать, определяется ПО ЭВрИСТИЧеСКОМу Правилу: ПОЛе vm_private_data
дескриптора vmaareastruct содержит текущий курсор в текущем сканиро-
сканировании. Это означает, что функция trytounmapfileo может в некоторых
случаях пропустить страницу, подлежащую обратному отображению. Когда
это происходит, функция trytounmap() обнаруживает, что страница по-
прежнему является отображающей, и возвращает swapagain вместо
SWAP_SUCCESS.
Реализация алгоритма PFRA
Алгоритм утилизации страничных кадров должен рассмотреть множество
страниц, принадлежащих процессам режима пользователя, дисковым кэшам
и кэшам памяти. Кроме того, он должен соблюдать несколько эвристических
правил. Неудивительно, что алгоритм PFRA состоит из большого количества
функций. На рис. 17.3 показаны основные функции этого алгоритма, а стрел-
стрелки обозначают вызовы функций. Например, функция trytofreepagesO
вызывает функции shrink_caches (), shrink_slab () И out_of_memory ().
Нетрудно заметить, что у алгоритма PFRA несколько "точек входа". Утили-
Утилизация страничного кадра выполняется в трех ситуациях:
П утилизация при дефиците памяти — ядро распознает ситуацию "дефицит
памяти";
□ утилизация при гибернации — ядро должно освободить память, потому
что переходит в состояние suspend-to-disk (мы больше не возвратимся к
этому случаю);
□ периодическая утилизация— поток ядра периодически активизируется
для утилизации памяти, если в ней имеется необходимость.
Утилизация при дефиците памяти активизируется в следующих случаях:
□ функция growbuf f ers (), вызванная функцией getbik (), не смогла выде-
выделить новую страницу буферов (см. разд. "Поиск блоков в кэше страниц"
главы 15);
П функция alloc_page_buffers(), вызванная функцией create_empty_
buffers о, не смогла выделить головы временных буферов для страницы
(см. разд. "Чтение и запись файла" главы 16);
□ функция aiiocpages () не смогла выделить группу смежных страничных
кадров в заданном списке зон памяти (см. главу 8).
Рис. 17.3. Основные функции алгоритма PFRA
Периодическая утилизация активизируется потоками ядра двух различных
типов:
□ потоки kswapd проверяют, не упало ли количество свободных страничных
кадров в некоторых зонах памяти ниже отметки pageslow (см.
разд. "Периодическая утилизация" далее в этой главе)',
П потоки events являются рабочими потоками предопределенной рабочей
очереди (см. главу 4). Алгоритм PFRA периодически запускает задание
для предопределенной рабочей очереди, чтобы утилизировать все свобод-
свободные участки в кэше памяти, которым управляет slab-аллокатор (см. гла-
главу 8).
Далее мы подробно опишем различные компоненты алгоритма утилизации
страничных кадров, включая все функции, показанные на рис. 17.3.
Списки давно неиспользуемых страниц (LRU)
Все страницы, принадлежащие адресному пространству процессов в режиме
пользователя или кэшу страниц, собраны в два списка, называемые активным
и неактивным; они также известны под общим названием списков давно не-
неиспользуемых страниц (списков LRU). Первый список содержит, по большей
части, страницы, к которым были обращения недавно, а второй включает в
себя страницы, к которым в течение какого-то времени ни один процесс не
обращался. Очевидно, что для утилизации следует брать страницы из неак-
неактивного списка.
Активный и неактивный списки страниц являются важнейшими структурами
данных для алгоритма утилизации страничных кадров. Головы этих двуна-
двунаправленных СПИСКОВ ХраНЯТСЯ, Соответственно, В ПОЛЯХ active_list И
inactivelist каждого Дескриптора zone (см. главу 8). ПОЛЯ nractive И
nrinactive у одного дескриптора содержат количество страниц в этих спи-
списках. Поле lruiock является спин-блокировкой, которая защищает эти два
списка от параллельного доступа в SMP-системах.
Если страница принадлежит одному из списков LRU, флаг PGiru в ее деск-
дескрипторе установлен. Кроме того, если страница принадлежит активному спи-
списку, установлен флаг PGactive, а если неактивному — этот флаг сброшен.
Поле lru дескриптора страницы содержит указатели на следующий и преды-
предыдущий элементы списка LRU.
Для работы со списками давно неиспользуемых страниц существует несколь-
несколько служебных функций:
□ addpagetoactiveiist о — добавляет страницу в начало активного спи-
списка зоны и увеличивает поле nractive дескриптора зоны;
□ addpagetoinactiveiisto — добавляет страницу в начало неактивного
списка зоны и увеличивает поле nrinactive дескриптора зоны;
□ deipagefromactiveiist () — удаляет страницу из активного списка зо-
зоны и уменьшает поле nractive дескриптора зоны;
□ deipagefrominactiveiist о — удаляет страницу из неактивного списка
зоны и уменьшает поле nrinactive дескриптора зоны;
□ del_page_frorn_lru() — проверяет флаг PGactive у страницы И, В зависи-
мости от результата, удаляет страницу из активного или неактивного спи-
списка, уменьшает поле nractive или nrinactive дескриптора зоны и, если
необходимо, сбрасывает флаг PGactive;
□ activate_page() — проверяет флаг PGactive. ЕСЛИ ОН сброшен (страница
находится в неактивном списке), функция переносит ее в активный спи-
список; вызывает функцию delpagef rom inactive list (), затем — функцию
add page to active_list() И, наконец, устанавливает флаг PGactive. До
переноса страницы функция получает спин-блокировку зоны lruiock;
П1 lrucacheaddo — если страница не включена ни в один список давно не-
неиспользуемых страниц, эта функция устанавливает флаг PGiru, получает
СПИН-блОКИрОВКу ЗОНЫ lruiock И ВЫЗЫВаеТ фуНКЦИЮ add_page_to_
inactiveiist (), чтобы занести страницу в неактивный список зоны;
□ iru_cache_add_active() — если страница не включена ни в один список
давно неиспользуемых страниц, эта функция устанавливает флаги PGiru и
PGactive, получает спин-блокировку зоны lruiock и вызывает функцию
add_page_to_active_list(), чтобы Занести Страницу В активный СПИСОК
зоны.
На самом деле, последние две функции немного сложнее. Они не заносят
страницу в список давно неиспользуемых страниц немедленно, а накаплива-
накапливают страницы во временных структурах типа pagevec, каждая из которых мо-
может вмещать до 14 указателей на дескрипторы страниц. Страницы фактиче-
фактически переносятся в список давно неиспользуемых страниц только тогда, когда
структура pagevec оказывается заполненной. Этот механизм повышает произ-
производительность системы, потому что спин-блокировка списка давно неисполь-
неиспользуемых страниц используется только при действительной модификации спи-
списков LRU.
Перенос страниц между списками LRU
Алгоритм PFRA собирает страницы, к которым процессы обращались недав-
недавно, в активный список, чтобы не сканировать их при поиске страничного кад-
кадра, подходящего для утилизации. Страницы, обращений к которым давно не
было, алгоритм собирает в неактивном списке. Конечно, в зависимости от
обстоятельств, страницы приходится переносить из одного списка в другой и
обратно.
Очевидно, что двух состояний страницы ("активная" и "неактивная") недос-
недостаточно для описания всех возможных вариантов обращения к страницам.
Предположим, некоторый процесс, ведущий журнал, записывает данные в
страницу один раз в час. Хотя страница является "неактивной" большую
часть времени, каждая операция записи делает ее "активной", запрещая ути-
утилизацию соответствующего страничного кадра, даже если в течение следую-
следующего часа обращений к странице наверняка не будет. Естественно, у такой
проблемы нет общего решения, поскольку у алгоритма утилизации странич-
страничных кадров нет способа предсказать поведение процессов режима пользова-
пользователя. Тем не менее представляется разумным, что статус страницы не должен
меняться на основании одного-единственного обращения к ней.
Флаг PGreferenced в дескрипторе страницы служит для удваивания количе-
количества обращений, необходимого для переноса страницы из неактивного списка
в активный. Он также служит для удваивания количества "недостающих" об-
обращений, необходимого для переноса страницы из активного списка в неак-
неактивный. Предположим, например, что у страницы в неактивном списке флаг
PGreferenced равен 0. Первое обращение к странице изменяет значение фла-
флага на 1, но страница остается в неактивном списке. При втором обращении к
странице обнаруживается, что флаг установлен, и страница перемещается в
активный список. Если же в течение определенного интервала времени вто-
второе обращение не произойдет, алгоритм утилизации страничных кадров мо-
может сбрОСИТЬ флаг PGreferenced.
Как видно из рис. 17.4, алгоритм утилизации страничных кадров вызывает
функции mark_page_accessed (), page_ref erenced () И refill_inactive_zone ()
для переноса страниц из одного списка в другой. На рисунке список LRU,
в котором находится страница, определяется состоянием флага PGactive.
Рис. 17.4. Перенос страниц между списками LRU
Функция mark_page_accessed()
Когда ядру нужно отметить факт обращения к странице, оно вызывает функ-
функцию mark_page_accessed(). Это ПРОИСХОДИТ каждый раз, КОГДа ЯДрО ОПредеЛЯ-
ет, что на страницу ссылается процесс режима пользователя, слой файловой
Системы ИЛИ драйвер устройства. В чаСТНОСТИ, функция mark_page_accessed()
вызывается в следующих случаях:
□ когда анонимная страница процесса загружается "по требованию" (дейст-
(действие выполняется функцией doanonymouspage (); см. главу 9);
□ когда страница файла, отображенного в память, загружается "по требова-
требованию" (действие выполняется функцией filemapnopageo; см. разд. "Выде-
"Выделение страниц по требованию для отображения в память"главы 16);
□ когда страница области памяти, совместно используемой для межпроцесс-
межпроцессного взаимодействия, загружается "по требованию" (действие выполняет-
выполняется функцией shmemnopage (); см. разд. "Совместно используемая память
межпроцессного взаимодействия" главы 19);
□ при чтении страницы данных из файла (действие выполняется функцией
dogenericf ileread (); см. разд. "Чтение из файла" главы 16);
П при загрузке выгруженной страницы (действие выполняется функцией
doswappage (); см. разд. "Загрузка выгруженных страниц" далее в этой
главе);
П при поиске страницы буферов в кэше страниц (см. описание функции
find_get_biock() в главе 15).
Функция markpageaccessed () выполняет следующий фрагмент кода:
if (IPageActive(page) && PageReferenced(page) && PageLRU(page)) {
activate_page(page);
ClearPageReferenced(page);
} else if (!PageReferenced(page))
SetPageReferenced(page);
Как показано на рис. 17.4, функция переносит страницу из неактивного спи-
списка в активный, только если до ее вызова флаг PGref erenced был установлен.
Функция page__referenced()
Функция pagereferencedo, которая вызывается один раз для каждой стра-
страницы, просканированной алгоритмом утилизации страничных кадров, воз-
возвращает 1, если установлен либо флаг PGref erenced, либо некоторые из би-
битов Accessed в записях Таблицы Страниц. В противном случае она возвраща-
возвращает 0. Эта функция вначале проверяет флаг PGref erenced дескриптора
страницы и, если флаг установлен, сбрасывает его. Затем она приводит в дей-
действие механизм объектно-базированного обратного отображения, чтобы про-
проверить и сбросить биты Accessed во всех записях Таблицы Страниц режима
пользователя, которые ссылаются на страничный кадр. Для этого функция
вызывает три служебные функции: page_referenced_anon(), page_referenced_
file () И pageref erencedone (), которые аналогичны функциям
trytounmapxxx (), описанным в разд. "Обратное отображение" ранее в этой
главе. Функция pagereferencedo также принимает во внимание жетон защи-
защиты от выгрузки (см. разд. "Жетон защиты от выгрузки" далее в этой главе).
Функция pageref erenced о никогда не переносит страницы из активного
СПИСКа В НеаКТИВНЫЙ; Эту работу ВЫПОЛНЯеТ фуНКЦИЯ refill_inactive_zone().
В действительности, она делает гораздо больше, чем простой перенос стра-
ниц из активного списка в неактивный, так что мы опишем ее более под-
подробно.
Функция refill_inactive_zone()
Как видно из рис. 17.3, функция refiii_inactive_zone() вызывается функци-
функцией shrinkzone (), которая утилизирует страницы, находящиеся в кэше стра-
страниц и в адресном пространстве режима пользователя (см. разд. 'Загрузка вы-
выгруженных страниц" далее в этой главе). Функция принимает два параметра:
указатель zone на дескриптор зоны памяти и указатель sc на структуру
scancontroi. Последняя структура активно используется алгоритмом утили-
утилизации страничных кадров и содержит информацию о текущей операции ути-
утилизации. Поля этой структуры перечислены в табл. 17.2.
Таблица 17.2. Поля структуры scan_control
Тип Поле Описание
unsigned long nr_to_scan Количество страниц, которое необходимо проскани-
ровать в активном списке
unsigned long nr_scanned Количество неактивных страниц, просканированных
во время текущей итерации
unsigned long nrreclaimed Количество страниц, утилизированных во время
текущей итерации
unsigned long nrmapped Количество страниц, на которые есть ссылки в ад-
адресном пространстве режима пользователя
int nr_to_reclaim Количество страниц, которое необходимо утилизи-
утилизировать
unsigned int priority Приоритет сканирования, от 12 до 0. Меньшее зна-
значение приоритета подразумевает сканирование
большего количества страниц
unsigned int gfp_mask Маска GFP, переданная от вызывающей функции
int may_writepage Если этот флаг установлен, запись "грязных" стра-
страниц на диск разрешена (только для режима "laptop
mode)
Роль функции refiiiinactivezoneo чрезвычайно важна, поскольку перенос
страницы из активного списка в неактивный делает страницу кандидатом на
утилизацию, в конечном счете, алгоритмом PFRA. Если функция будет вести
5 Специализированный режим работы для ноутбуков, организующий физическую запись на диск
таким образом, чтобы она производилась как можно реже, и контроллер жесткого диска мог выклю-
выключить его двигатель. Способствует уменьшению энергопотребления. — Прим. науч. ред.
себя слишком агрессивно, она перенесет из активного списка в неактивный
очень много страниц. Как следствие, алгоритм PFRA утилизирует большое
количество страничных кадров, и система достигнет высокой производитель-
производительности. С другой стороны, если функция будет слишком "ленивой", неактив-
неактивный список не будет пополняться достаточным количеством неиспользуемых
страниц, и алгоритму PFRA не удастся утилизировать память. Таким обра-
образом, функция ведет себе адаптивно: поначалу она при каждом вызове скани-
сканирует небольшое количество страниц в активном списке. Однако, если у алго-
алгоритма PFRA возникнут проблемы с утилизацией страничных кадров, функ-
функция refiiiinactivezoneo станет увеличивать количество сканируемых
страниц в активном списке при каждом вызове. Такое поведение управляется
значением поля priority в структуре scancontroi (чем меньше значение, тем
выше приоритет).
Есть еще одно правило, регулирующее поведение функции refiii_inactive_
zone (). Списки LRU содержат два вида страниц: страницы, принадлежащие
адресным пространствам режима пользователя, и страницы, включенные в
кэш, но не принадлежащие ни одному процессу в режиме пользователя. Как
было сказано ранее, алгоритм утилизации страничных кадров должен ста-
стараться "ужимать" кэш страниц, оставляя в памяти страницы, принадлежащие
процессам, работающим в режиме пользователя. Однако никакое зафиксиро-
зафиксированное "золотое правило" не в состоянии обеспечить хорошую производи-
производительность В любой Ситуации, И ПОЭТОМУ функция refill_inactive_zone() ИС-
пользует эвристическое значение тенденции к выгрузке. Это значение опре-
определяет, будет ли функция перемещать все виды страниц или только
страницы, не принадлежащие адресным пространствам режима пользовате-
пользователя6. Значение тенденции к выгрузке вычисляется функцией по следующей
формуле:
тенденция к выгрузке = отношение отображения / 2 + степень неблаго-
неблагополучия + выгружаемостъ
Здесь отношение отображения— процентное отношение страниц из всех
зон памяти, принадлежащих адресным пространствам режима пользователя
(sc->nr_mapped), к общему количеству выделяемых страничных кадров. Вы-
Высокое значение отношения отображения свидетельствует о том, что динами-
динамическая память используется, в основном, процессами, работающими в режи-
режиме пользователя; низкое значение говорит о том, что память преимуществен-
преимущественно используется кэшем страниц.
6 Название "тенденция к выгрузке" является не вполне точным, так как страницы в адресных про-
пространствах режима пользователя могут быть выгружаемыми, синхронизируемыми или страницами
на выброс. Тем не менее значение тенденции к выгрузке определенно контролирует объем выгрузки,
выполняемой алгоритмом PFRA, потому что почти все выгружаемые страницы принадлежат адрес-
адресным пространствам режима пользователя.
Степень неблагополучия является мерой того, насколько эффективно алго-
алгоритм PFRA утилизирует страничные кадры в данной зоне. Это значение ос-
основано на приоритете сканирования зоны в предыдущем проходе алгоритма
(приоритет хранится в поле prevpriority дескриптора zone). Значение степе-
степени неблагополучия зависит от предыдущего приоритета зоны следующим
образом:
Предыдущий 12...7 6 5 4 3 2 10
приоритет зоны
Степень 0 1 3 6 12 25 50 100
неблагополучия
Выгружаемостъ — это константа, определяемая пользователем. Обычно она
равна 60. Системный администратор может непосредственно изменить это
значение в файле /proc/sys/vm/swappiness или изменить его с помощью сис-
системного вызова syscti ().
Страницы будут отозваны из адресных пространств процессов для утилиза-
утилизации, только если тенденция к выгрузке из данной зоны станет больше или
равной 100. Таким образом, если системный администратор приравняет вы-
гружаемость к нулю, алгоритм PFRA не будет утилизировать страницы из
адресных пространств режима пользователя, если предыдущий приоритет
зоны не станет равным нулю (что маловероятно). Если же администратор ус-
установит выгружаемость в значение 100, алгоритм будет утилизировать стра-
страницы из адресных пространств режима пользователя при каждом вызове.
Приведем краткое описание действий, выполняемых функцией refiii_
inactive_zone() I
1. Вызывает функцию lruadddrain (), чтобы перенести в активный и неак-
неактивный списки все страницы, которые еще находятся в структурах
pagevec.
2. Получает СПИН-блОКИрОВКу zone->lru_lock.
3. Выполняет первый цикл сканирования страниц в списке zone->
activeiist, начиная с хвоста списка и двигаясь в обратном направлении.
Продолжает, пока список не станет пустым или пока не будет просканиро-
вано sc->nr_to_scan страниц. Для каждой страницы, просканированной в
этом цикле, функция увеличивает счетчик ссылок на единицу, удаляет де-
дескриптор страницы из списка zone->active_iist и заносит его во времен-
временный локальный список lhoid. Однако, если счетчик ссылок страничного
кадра был равен нулю, функция возвращает страницу в активный список.
Дело в том, что страничные кадры с нулевым счетчиком ссылок должны
принадлежать buddy-системе данной зоны. Для освобождения странично-
го кадра следует вначале уменьшить его счетчик ссылок, а затем удалить
страничный кадр из списков давно неиспользуемых страниц и занести его
в список buddy-системы. Следовательно, у алгоритма PFRA есть неболь-
небольшое "окно" времени, в течение которого он может увидеть свободную
страницу в одном из списков LRU.
4. Прибавляет к значению zone->pages_scanned количество активных стра-
страниц, которые были просканированы.
5. Вычитает из значения zone->nr_active количество страниц, перенесенных
в локальный список lhoid.
6. Освобождает СПИН-блОКИрОВКу zone->lru_lock.
7. Вычисляет значение тенденции к выгрузке.
8. Выполняет второй цикл для страниц в локальном списке lhoid. Цель
этого цикла состоит в разделении страниц из списка lhoid на два под-
подсписка: lactive и linactive. Страница, принадлежащая адресному про-
пространству какого-либо процесса в режиме пользователя (то есть страни-
страница, у которой значение page->_mapcount не отрицательно), добавляется в
список lactive, если тенденция к выгрузке меньше ста, или если страни-
страница является анонимной, но никакая область подкачки не активна, или ес-
если функция pagereferencedo, примененная к странице, возвратила по-
положительное число, означающее, что недавно произошло обращение к
странице. Во всех остальных случаях страница заносится в список
l_inactive .
9. Получает спин-блокировку zone->iru_iock.
10. Выполняет третий цикл для страниц в локальном списке linactive, что-
чтобы перенести ИХ В СПИСОК zone->inactive_list, И обновляет ПОЛе zone->
nrinactive. При этом функция уменьшает счетчики обращений перено-
переносимых страничных кадров, чтобы компенсировать увеличение счетчиков,
имевшее место на шаге 3.
11. Выполняет четвертый и последний цикл для страниц в локальном списке
lactive, Чтобы перенести ИХ В СПИСОК zone->active_list, И обновляет
поле zone->nr_active. При этом функция уменьшает счетчики обращений
переносимых страничных кадров, чтобы компенсировать увеличение
счетчиков, имевшее место на шаге 3.
12. Освобождает спин-блокировку zone->iru_iock и возвращает управление.
7 Обратите внимание, что страница, не принадлежащая адресному пространству процессов режима
пользователя, переносится в неактивный список. Однако, поскольку ее флаг PGreferenced не
сброшен, первое же обращение к ней заставляет функцию mark_page_accessed () вернуть эту
страницу в активный список.
Следует отметить, что функция refiiiinactivezoneo проверяет флаг
pg referenced только у страниц, принадлежащих адресным пространствам
режима пользователя (см. шаг 8). В противном случае страницы находятся в
хвосте активного списка (из чего следует, что к ним происходили обращения
некоторое время тому назад, и маловероятно, что обращения к ним произой-
произойдут в ближайшем будущем). С другой стороны, функция не переносит стра-
страницу из активного списка, если ею владеет какой-либо процесс в режиме
пользователя, и он недавно к ней обращался.
Утилизация при дефиците памяти
Утилизация при дефиците памяти активизируется, когда попытка выделения
памяти заканчивается неудачей. Как показано на рис. 17.3, ядро вызывает
функцию f reemorememory () при выделении буфера виртуальной файловой
системы или головы буфера, а функцию trytof reepages () оно вызывает
при выделении одной или нескольких страниц из buddy-системы.
Функция treejmorejmemoryQ
Функция f reemorememory () ВЫПОЛНЯет Следующие деЙСТВИЯ*.
1. Вызывает функцию wakeup_bdflush (), чтобы активизировать поток ядра
pdflush и запустить операции записи для 1024 грязных страниц в кэше (см.
разд. "Потоки ядра pdflush" главы 15). Запись грязных страниц на диск
может, в конечном счете, позволить освободить страничные кадры, со-
содержащие буферы, головы буферов и прочие структуры виртуальной
файловой системы.
2. Вызывает служебную процедуру системного вызова schedyieido, чтобы
предоставить потоку ядра pdflush возможность выполнения.
3. Запускает цикл по всем узлам памяти (см. главу 8). Для каждого узла
функция вызывает функцию trytof reepages (), передавая ей список
"дефицитных" зон памяти (в архитектуре 80x86 это zonedma и
zone_normal).
Функция try_to_Jree__pages()
Функция trytof reepages () принимает три параметра:
□ zones — список зон памяти, в которых должны быть утилизированы стра-
страницы;
□ gf pmask — набор флагов, которые были использованы при неудавшейся
попытке выделить память;
□ order — не используется.
Цель этой функции состоит в том, чтобы освободить, как минимум,
32 страничных кадра, многократно вызывая функции shrink caches о и
shrinksiabo, каждый раз с приоритетом более высоким, чем при предыду-
предыдущем вызове. Эти вспомогательные функции находят значение приоритета
(как и другие параметры текущей операции сканирования) в дескрипторе ти-
типа scancontroi (см. табл. 17.2). Наименьший, первоначальный приоритет
равен 12, в то время как самый высокий, конечный приоритет равен 0. Если
функция tryto freepageso не сумеет утилизировать хотя бы 32 стра-
страничных Кадра На ОДНОМ ИЗ 13 ВЫЗОВОВ фуНКЦИЙ shrink_caches() И
shrinkslab (), алгоритм утилизации страничных кадров окажется в затрудни-
затруднительном положении, и в его распоряжении останется последнее средство:
уничтожить какой-нибудь процесс, чтобы освободить все его страничные
кадры. Эта операция выполняется функцией outof memory () (см. разд. "Унич-
"Уничтожение процессов из-за нехватки памяти" далее в этой главе).
Функция выполняет следующие действия:
1. Выделяет и инициализирует дескриптор scancontroi. В частности, сохра-
сохраняет маску Выделения gfpjnask В ПОЛе gfpjnask.
2. Для каждой зоны в списках zones функция записывает в поле
tempjpriority дескриптора зоны первоначальное значение приоритета
A2). Кроме того, она вычисляет суммарное количество страниц в списках
LRU этих зон.
3. Выполняет цикл максимум из 13 итераций, соответствующих приоритетам
от 12 до 0; при каждой итерации выполняет следующие действия:
• обновляет некоторые поля дескриптора scan control. В частности, за-
записывает в поле nrmapped суммарное количество страниц, которыми
владеют процессы режима пользователя, а в поле priority — приори-
приоритет текущей итерации. Кроме того, функция записывает нули в поля
nr_scanned И nr_reclaimed;
• вызывает функцию shrinkcaches (), передавая ей в качестве аргумен-
аргументов список zones и адрес дескриптора scancontroi. Эта функция ска-
сканирует неактивные страницы зон;
• вызывает функцию shrinksiabo, чтобы утилизировать страницы из
сокращаемых кэшей ядра (см. разд. "Утилизация страниц сокращае-
сокращаемых кэшей диска" далее в этой главе)';
• еСЛИ ПОЛе current->reclaim_state не содержит NULL, функция прибавля-
ет к значению в поле nrreciaimed дескриптора scancontroi количест-
количество страниц, утилизированных из кэшей slab-аллокатора. Это значение
хранится в небольшой структуре данных, на которую указывает поле
дескриптора процесса. Функция aiiocpages о устанавливает поле
current->reclaim_state перед ВЫЗОВОМ функции try_to_f ree_pages () И
сбрасывает это поле сразу после завершения работы функции. Странно,
но функция f reemorememory () это поле не устанавливает;
• если цель достигнута (поле nrreciaimed дескриптора scancontroi
больше или равно 32), функция прерывает цикл и переходит к шагу 4;
• цель еще не достигнута. Если к этому моменту просканировано, как
МИНИМум, 49 СТраНИЦ, фуНКЦИЯ ВЫЗЫВает фунКЦИЮ wakeup_bdflush (),
чтобы активизировать поток ядра pdflush и записать некоторые грязные
страницы из кэша на диск (см. разд. "Поиск грязных страниц для запи-
записи на диск" в главе 15);
• если функция уже выполнила четыре итерации, не достигнув цели, она
вызывает функцию bikcongestionwait о, чтобы приостановить теку-
текущий процесс, пока очередь запросов write не перестанет быть перепол-
переполненной, или пока не пройдет 100 мс (см. разд. "Дескрипторы запросов"
главы 14).
4. Записывает в поле prevpriority каждого дескриптора зоны приоритет,
использованный при последнем вызове функции shrinkcaches (). Значе-
Значение приоритета хранится в поле temppriority дескриптора зоны.
5. Возвращает 1, если утилизация прошла успешно, и 0 в противном случае.
Функция shrink_caches()
ФуНКЦИЯ shrinkcaches () вызывается функцией trytof reepages (). Она
принимает два параметра: список зон памяти zones и адрес sc дескриптора
scan_control.
Цель этой функции заключается лишь в том, чтобы вызвать функцию
shrinkzone () для каждой зоны из списка zones. Однако перед вызовом функ-
функции shrinkzone () ДЛЯ данной ЗОНЫ функция shrinkcaches () обновляет ПОЛе
temppriority дескриптора зоны, пользуясь значением из поля sc->
priority — это текущий приоритет операции сканирования. Кроме того, если
значение приоритета при предыдущем вызове алгоритма PFRA было выше
текущего, т. е. утилизацию страничных кадров в этой зоне сейчас выполнить
труднее, то функция shrinkcaches () копирует текущий приоритет в поле
prevpriority дескриптора зоны. Наконец, функция shrinkcaches () не вызы-
вызывает функцию shrinkzone () ДЛЯ данной ЗОНЫ, если флаг all_unreclaimable В
дескрипторе зоны установлен, а текущий приоритет меньше 12. Иными сло-
словами, функция shrinkcaches () не вызывается при первой итерации цикла в
функции trytof reepages (). Алгоритм утилизации страничных кадров ус-
танавливает флаг aii_unreciaimabie, когда приходит к выводу, что в зоне так
много неутилизируемых страниц, что сканирование их является пустой тра-
тратой времени.
Функция shrink_zone()
Функция shrinkzone () принимает два параметра: zone, указатель на дескрип-
дескриптор structzone, и sc, указатель на дескриптор scancontroi. Цель этой функ-
функции заключается в том, чтобы утилизировать 32 страницы из неактивного
списка зоны. Функция пытается добиться этого, многократно вызывая слу-
служебную функцию shrinkcache (), каждый раз для большего фрагмента неак-
неактивного списка зоны. Кроме того, функция shrinkzone () пополняет неактив-
неактивный СПИСОК ЗОНЫ, МНОГОКраТНО ВЫЗЫВаЯ фуНКЦИЮ refill_inactive_zone(),
описанную ранее в разд. "Списки давно неиспользуемых страниц (LRU)".
Поля nrscanactive и nrscaninactive дескриптора зоны играют здесь осо-
бую роль. Из соображений эффективности функция работает с пакетами по
32 страницы. Таким образом, если она выполняется с низким уровнем приви-
привилегий (с высоким значением в поле sc->priority), а один из списков давно
неиспользуемых страниц не содержит достаточное количество элементов,
функция пропускает сканирование этого списка. Впрочем, количество про-
пропущенных при этом активных или неактивных страниц записывается в поля
nrscanactive ИЛИ nrscaninactive, Так ЧТО пропущенные страницы будут
рассмотрены при следующем вызове функции.
Если говорить конкретно, функция shrinkzone () выполняет следующие дей-
действия:
1. Увеличивает поле zone->nr_scan_active на некоторую часть от общего ко-
количества элементов в активном списке (zone->nr_active). Фактическое
значение приращения определяется текущим приоритетом и колеблется в
диапазоне от zone->nr_active/212 до zone->nr_active/2° (то есть до общего
количества активных страниц в зоне).
2. Увеличивает поле zone->nr_scan_inactive на некоторую часть от общего
количества элементов в неактивном списке (zone->nr_inactive). Фактиче-
Фактическое значение приращения определяется текущим приоритетом и колеб-
колеблется В диапазоне ОТ zone->nr_inactive/2 ДО zone->nr_inactive.
3. Если значение в поле zone->nr_scan_active больше или равно 32, функция
копирует его в локальную переменную nractive, а поле сбрасывает до
нуля. В противном случае функция записывает ноль в переменную
nr_active.
4. Если значение в поле zone->nr_scan_inactive больше или равно 32, функ-
функция копирует его в локальную переменную nrinactive, а поле сбрасывает
до нуля. В противном случае функция записывает ноль в переменную
nr_inactive.
5. Записывает 32 в поле sc->nr_to_reciaim дескриптора scancontroi.
6. Если оба поля nractive и nrinactive равны О, ничего не надо делать, и
функция завершает работу. Это бывает в маловероятной ситуации, когда
процессам в режиме пользователя не выделено ни одного страничного
кадра.
7. Если значение поля nractive положительно, функция пополняет неактив-
неактивный список зоны:
sc->nr_to_scan = min(nr_active, 32);
nr_active -= sc->nr_to_scan;
refill_inactive_zone(zone, sc) ;
8. Если значение поля nrinactive положительно, функция пытается утили-
утилизировать хотя бы 32 страницы из неактивного списка:
sc->nr_to_scan = min (nr_inactive, 32);
nr_inactive -= sc->nr_to_scan;
shrink_cache(zone, sc);
9. Если функции shrinkzone () удается утилизировать 32 страницы (значе-
(значение в поле sc->nr_to_reciaim теперь равно нулю или отрицательно), функ-
функция завершает работу. В противном случае она переходит к шагу 6.
Функция shrink_cache()
Функция shrinkcache () является еще одной служебной функцией, основная
цель которой состоит в извлечении группы страниц и неактивного списка зо-
зоны, занесении их во временный список и вызове функции shrinkiist о, ко-
которая выполнит фактическую утилизацию страничных кадров для всех стра-
страниц во временном списке. Функция shrinkcache () принимает те же парамет-
параметры, ЧТО И фуНКЦИЯ shrinkzones (), а Именно zone И sc, И ВЫПОЛНЯеТ
следующие действия:
1. Вызывает функцию iru_add_drain(), чтобы перенести в активный и неак-
неактивный списки страницы, все еще находящиеся в структурах pagevec (см.
разд. "Списки давно неиспользуемых страниц (LRU) "ранее в этой главе).
2. Получает спин-блокировку zone->iru_iock.
3. Рассматривает как минимум 32 страницы в неактивном списке. Для каж-
каждой страницы функция увеличивает счетчик обращений, проверяет, не ос-
освобождается ли страница для buddy-системы и переносит страницу из не-
неактивного списка зоны в локальный список.
4. Уменьшает счетчик zone->nr_inactive на количество страниц, перенесен-
перенесенных из неактивного списка.
5. Увеличивает счетчик zone->pages_scanned на количество страниц, факти-
фактически просмотренных в неактивном списке.
6. Освобождает СПИН-блОКИрОВКу zone->lru_lock.
7. Вызывает функцию shrinkiist о, передавая ей локальный список стра-
страниц, составленный на шаге 3. Эта функция (как, без сомнения, догадался
читатель) обсуждается далее.
8. Уменьшает значение поля sc->nr_to_reciaim на количество страниц, фак-
фактически уТИЛИЗИрОВаННЫХ функцией shrinkiist ().
9. Снова получает спин-блокировку zone->iru_iock.
10. Возвращает в неактивный или активный список все страницы из локаль-
локального списка, которые функция shrinkiist о не смогла освободить. Об-
Обратите внимание, что функция shrinkiist () могла пометить некоторые
страницы как активные, установив у них флаги PGactive. Эта операция
выполняется над пакетом страниц с использованием структуры данных
pagevec (см. разд. "Списки давно неиспользуемых страниц (LRU)"ранее в
этой главе).
11. Если функция просканировала как минимум sc->nr_to_scan страниц, но
ей не удалось утилизировать заданное количество страниц (то есть значе-
значение в поле sc->nr_to_reciaim все еще положительно), она возвращается к
шагу 3.
12. Освобождает спин-блокировку zone->iru_iock и завершает работу.
Функция shrinkJIstQ
Мы добрались до сердцевины утилизации страниц. В то время как цель функ-
функций, описанных ДО ЭТОГО момента, ОТ try_to_free_pages() ДО shrink cache (),
заключалась в составлении набора страниц-кандидатов на утилизацию,
функция shrinkiisto пытается выполнить фактическую утилизацию стра-
страниц из списка, переданного ей в качестве параметра pageiist. Второй пара-
параметр sc, как обычно, является указателем на дескриптор scancontroi. Когда
функция shrinkiist о возвращает управление, список pageiist содержит
страницы, которые не удалось освободить.
Эта функция выполняет следующие действия:
1. Если поле-флаг tifneedresched текущего процесса установлено, функ-
функция ВЫЗЫВаеТ фуНКЦИЮ schedule () .
2. Запускает цикл по всем дескрипторам страниц из списка page list. При
каждой итерации она удаляет дескриптор страницы из списка и пытается
утилизировать страничный кадр. Если это по какой-то причине не удается,
функция заносит дескриптор страницы в локальный список.
3. На этом шаге список pageiist пуст. Функция возвращает в него дескрип-
дескрипторы страниц из локального списка.
4. Увеличивает значение в поле sc->nr_reciaimed на количество страничных
кадров, утилизированных на шаге 2, и возвращает это число.
Конечно, самой интересной частью функции shrinkiist о является код, пы-
пытающийся утилизировать страничный кадр. Его блок-схема приведена на
рис. 17.5.
Существует только три возможных исхода у обработки каждого кадра функ-
функцией shrink_list():
□ страница возвращается buddy-системе зоны при помощи функции
f reecoidpage () (см. главу 8). Иными словами, страница успешно утили-
утилизируется;
□ страница не утилизируется. Она будет заново занесена в список pageiist,
но функция shrinkiisto предполагает, что в ближайшем будущем стра-
страницу удастся освободить. Поэтому функция оставляет сброшенным флаг
PGactive в дескрипторе страницы, чтобы страница была возвращена в не-
неактивный список зоны памяти. Это событие соответствует прямоугольни-
прямоугольникам "НЕАКТИВНАЯ" на рис. 17.5;
□ страница не утилизируется. Она будет заново занесена в список pageiist.
Либо страница активно используется, либо функция shrinkiist о пред-
предполагает, что страницу невозможно утилизировать в обозримом будущем.
Тогда функция устанавливает флаг PGactive в дескрипторе страницы,
чтобы страница была занесена в активный список зоны памяти. Это собы-
событие соответствует прямоугольникам "АКТИВНАЯ" на рис. 17.5.
Функция shrinkiisto никогда не станет утилизировать заблокированную
страницу (у которой установлен флаг PGiocked) или страницу, записываемую
на диск (установлен флаг PGwriteback). Чтобы проверить, были ли к страни-
странице обращения за последнее время, функция shrinkiist о вызывает функцию
pagereferenced (), описанную ранее в этой главе.
Чтобы утилизировать анонимную страницу, ее надо добавить в кэш подкач-
подкачки, и для нее должен быть зарезервирован новый слот в области подкачки
(подробности см. в разд. "Подкачка" далее в этой главе).
Если страница находится в адресном пространстве какого-либо процесса в
режиме пользователя (поле mapcount дескриптора страницы больше или рав-
равно нулю), функция shrinkiisto вызывает функцию trytounmapo, чтобы
найти все записи Таблицы Страниц в режиме пользователя, которые ссыла-
ссылаются на данный страничный кадр (см. разд. "Обратное отображение"ранее
в этой главе). Конечно, утилизация может продолжаться, только если эта
функция возвратит swapsuccess.
Рис. 17.5. Функция shrink_list (): логика утилизации страницы
Грязная страница не может быть утилизирована, если она не записана на
диск. Чтобы сделать это, функция shrinkiisto вызывает функцию
pageoutо, описанную далее. Утилизация страничного кадра может продол-
продолжаться, только если функция pageout () не должна выполнять операцию запи-
записи или если операция записи быстро заканчивается.
Если страница содержит буферы виртуальной файловой системы, функция
shrinklist () вызывает функцию trytoreleasepage (), Чтобы освободить
ассоциированные со страницей головы буферов (см. разд. "Освобождение
страниц буферов блочных устройств" главы 15).
Наконец, если все прошло гладко, функция shrink list о проверяет счетчик
ссылок страницы. Если он равен двум, значит, у страницы всего два владель-
владельца — кэш страниц (или кэш подкачки, если страница анонимная) и сам алго-
алгоритм утилизации страничных кадров (счетчик ссылок был увеличен на шаге 3
функции shrinkcache о). В этом случае страница может быть утилизирована
при условии, что она все еще не грязная. Для утилизации страница вначале
удаляется из кэша страниц или из кэша подкачки, в соответствии со значени-
значением флага PGswapcache дескриптора страницы, а затем выполняется функция
free_cold_page().
Функция pageoutQ
Функция pageout () вызывается функцией shrinklist (), когда грязная стра-
страница должна быть записана на диск. Эта функция выполняет следующие опе-
операции:
1. Проверяет, находится ли страница в кэше страниц или в кэше подкачки
(см. разд. "Кэш подкачки" далее в этой главе). Кроме того, убеждается,
что страницей владеют только кэш страниц (или кэш подкачки) и алго-
алгоритм PFRA. Функция возвращает значение page keep, если проверка за-
закончилась неудачей (нет смысла записывать страницу на диск, если
shrinklist () не может ее утилизировать).
2. Проверяет, определен ли метод writepage объекта addressspace. Если не
определен, возвращает page_activate.
3. Проверяет, может ли текущий процесс выдавать запросы на запись в оче-
очереди запросов блочного устройства, ассоциированного с объектом
addressspace. Дело в том, что только потоки ядра kswapd и pdflush всегда
могут выдавать запросы на запись. Что касается нормальных процессов,
они могут выдавать запросы, когда очередь запросов не переполнена, если
только поле current->backing_dev_info не указывает на структуру
backingdevinfo блочного устройства (см. разд. "Запись в файл" главы 16).
4. Проверяет, является ли страница все еще грязной. Если нет, возвращает
PAGE_CLEAN.
5. Устанавливает дескриптор writeback_control И ВЫЗЫВает метод writepage
объекта addressspace, чтобы запустить операцию записи страницы обрат-
обратно на диск (см. разд. "Запись грязных страниц на диск" главы 16).
6. Если метод writepage возвратил код ошибки, функция возвращает значе-
значение PAGE_ACTIVATE.
7. Возвращает page_success.
Утилизация страниц сокращаемых кэшей диска
Из предыдущих глав мы знаем, что, помимо кэша страниц, ядро пользуется
другими кэшами диска, например, кэшем элементов каталога и кэшем ин-
индексных дескрипторов (см. разд. "Кэш элементов каталога" главы 12). Когда
алгоритм PFRA пытается утилизировать страничные кадры, он должен также
проверить, можно ли сократить размер каких-нибудь кэшей диска.
Каждый кэш диска, рассматриваемый алгоритмом утилизации страничных
кадров, должен иметь сокращающую функцию, регистрируемую на этапе
инициализации. Сокращающая функция принимает два параметра: количест-
количество страничных кадров, которое должно быть утилизировано, и набор флагов
выделения GFP. Функция выполняет все, что необходимо для утилизации
страниц из кэша диска, а затем возвращает количество утилизируемых стра-
страниц, оставшихся в кэше.
Функция setshrinker () регистрирует сокращающую функцию для алгорит-
алгоритма утилизации страничных кадров. Эта функция выделяет дескриптор,
имеющий тип shrinker, сохраняет адрес сокращающей функции в этом деск-
дескрипторе, а затем заносит дескриптор в глобальный список с корнем в гло-
глобальной переменной shrinkeriist. Функция setshrinker о также инициали-
инициализирует поле seeks дескриптора shrinker. Говоря неформально, это параметр,
который показывает "стоимость" восстановления одного элемента кэша, если
он будет удален.
В Linux 2.6.11 не так уж много кэшей диска, зарегистрированных для алго-
алгоритма утилизации страничных кадров. Кроме кэша элементов каталога и кэ-
кэша индексных дескрипторов, только слой квоты диска, кэш блоков метаин-
формации файловой системы (используемый, в основном, для расширенных
атрибутов файловой системы) и журналируемая файловая система XFS реги-
регистрируют сокращающие функции.
Функция алгоритма PFRA, которая утилизирует страницы из сокращаемых
кэшей диска, называется shrinksiabo (название сбивает с толку, поскольку
функция не имеет никакого отношения к кэшам slab-аллокатора). Эта функ-
функция вызывается функцией trytofreepages о, как было сказано ранее в
разд. "Утилизация при дефиците памяти", и функцией balancejpgdatо, ко-
которая будет описана позже, в разд. "Периодическая утилизация".
Функция shrinksiabo пытается сбалансировать стоимость утилизации стра-
страницы из сокращаемого кэша диска со стоимостью утилизации из списков
LRU (выполняемой функцией shrinkiist ()). Если говорить коротко, функ-
функция проходит по списку дескрипторов shrinker, чтобы вызывать сокращаю-
сокращающие функции и получить общее количество утилизируемых страниц в кэшах
диска. Затем функция снова сканирует список дескрипторов shrinker. На
этот раз она для каждого сокращаемого кэша диска эвристическим образом
вычисляет количество страничных кадров, подлежащих утилизации (основы-
(основываясь на количестве утилизируемых страниц в кэшах диска, на относительной
стоимости восстановления страницы в кэше диска и на количестве страниц в
списках LRU), и вызывает сокращающую функцию, чтобы попытаться ути-
утилизировать пакеты минимум по 128 страниц.
За недостатком места, мы ограничимся лишь кратким описанием сокращаю-
сокращающих функций кэша элементов каталога и кэша индексных дескрипторов.
Утилизация страниц из кэша элемента каталога
Функция shrinkdcachememory () является сокращающей для кэша элементов
каталога. Она ищет в кэше неиспользуемые элементы каталога, т. е. объекты,
на которые не ссылается ни один процесс (см. главу 12) и освобождает их.
Поскольку объекты в кэше элементов каталога выделяются с помощью slab-
аллокатора, функция shrinkdcachememory() может освободить несколько
участков памяти, в результате чего некоторые страничные кадры будут ути-
утилизированы функцией cachereapo (см. разд. "Периодическая утилизация"
далее в этой главе). Кроме того, кэш элементов каталога действует как кон-
контроллер кэша индексных дескрипторов. Следовательно, когда элемент ката-
каталога освобождается, страницы, содержащие соответствующий индексный
дескриптор, могут стать "неиспользуемыми" и, в конечном счете, будут осво-
освобождены.
Функция shrinkdcachememory () принимает в качестве параметров количест-
количество страничных кадров, которое нужно освободить, и GFP-маску. Она начина-
начинает работу с проверки, сброшен ли бит gfpfs в GFP-маске. Если сброшен,
функция возвращает-1, потому что освобождение элемента каталога может
запустить какую-либо операцию в дисковой файловой системе. Собственно
утилизация страничных кадров выполняется функцией prunedcache (). Эта
функция сканирует список неиспользуемых элементов каталога (чья голова
хранится в переменной dentryunused), пока не достигнуто требуемое количе-
количество освобожденных объектов, или не будет просканирован весь список. Для
каждого объекта, к которому давно не было обращений, функция выполняет
следующие действия:
1. Удаляет объект "элемент каталога" из хеш-таблицы элементов каталога, из
списка элементов каталога в его родительском каталоге и из списка эле-
элементов каталога индексного дескриптора-владельца.
2. Уменьшает счетчик обращений индексного дескриптора элемента катало-
каталога, для чего вызывает метод diput, если он определен для данного эле-
элемента каталога, или функцию iput ().
3. Вызывает метод dreiease элемента каталога, если метод определен.
4. Вызывает функцию caiircuO для регистрации функции обратного вызо-
вызова, которая удалит элемент каталога (см. главу 5). В свою очередь, функ-
функция обратного вызова воспользуется функцией kmem_cache_f гее (), чтобы
вернуть объект slab-аллокатору (см. главу 8).
5. Уменьшает счетчик обращений родительского каталога.
В конце своей работы функция shrinkdcachememory () возвращает значение,
зависящее от количества неиспользуемых элементов каталога, оставшихся в
кэше. Более точно, возвращенное значение равно количеству неиспользуе-
неиспользуемых элементов каталога, умноженному на 100 и разделенному на значение
глобальной переменной sysctivf scachepressure. По умолчанию эта пере-
переменная равна 100, и возвращенное значение в точности равно количеству не-
неиспользуемых элементов каталога. Однако системный администратор может
модифицировть эту переменную в файле /proc/sys/vm/vfs_cache_pressure или
с помощью системного вызова sysctio. Если установить эту переменную в
значение, меньшее 100, функция shrinksiabo утилизирует меньше страниц
из кэша элементов каталога (и кэша индексных дескрипторов) по отношению
к количеству страниц, утилизированных из списков LRU. И наоборот, уста-
установка этой переменной в значение, большее 100, заставляет функцию
shrinksiabo утилизировать больше страниц кэша элементов каталога и кэ-
кэша индексных дескрипторов по отношению к количеству страниц, утилизи-
утилизированных из списков LRU.
Утилизация страниц из кэша индексных дескрипторов
Функция shrinkicachememory () вызывается для удаления неиспользуемых
объектов "индексный дескриптор" из кэша индексных дескрипторов. Здесь
термин "неиспользуемый" означает, что у индексного дескриптора больше
нет контролирующего объекта "элемент каталога". Эта функция аналогична
функции shrinkdcachememory(), описанной ранее. Она проверяет бит
GFPFS В параметре gfp_mask, затем вызывает функцию prune_icache() И,
наконец, возвращает значение, зависящее от количества неиспользуемых ин-
дексных дескрипторов, оставшихся в кэше, и от значения переменной
sysctivf scachepressure, как было описано ранее.
ЧТО КасаеТСЯ фуНКЦИИ prune_icache (), ОНа Сканирует СПИСОК inode_unused (СМ.
разд. "Индексный дескриптор" главы 12). Чтобы освободить индексный
дескриптор, функция освобождает любой закрытый буфер, ассоциированный
с индексным дескриптором, делает недействительными чистые страничные
кадры в кэше страниц, ссылающиеся на индексный дескриптор и более не
ИСПОЛЬЗуемые, а Затем вызывает фуНКЦИИ clear_inode() И destroy_inode()
для уничтожения индексного дескриптора.
Периодическая утилизация
Алгоритм PFRA выполняет периодическую утилизацию, применяя два раз-
различных механизма: потоки ядра kswapd, которые вызывают функции
shrinkzone () И shrinkslab (), Чтобы утилизировать СТраНИЦЫ ИЗ СПИСКОВ
LRU, и функцию cachereap (), которая вызывается периодически для утили-
утилизации неиспользуемых участков памяти slab-аллокатора.
Потоки ядра kswapd
Потоки ядра kswapd являются еще одним механизмом ядра, который активи-
активизирует утилизацию страничных кадров. Зачем он нужен? Разве недостаточно
вызвать функцию try_to_free_pages(), когда возникнет реальная нехватка
свободной памяти, и будет выдан очередной запрос на ее выделение?
К сожалению, все не так просто. Некоторые запросы на выделение памяти
делаются обработчиками прерываний и исключений, которые не могут за-
заблокировать текущий процесс в ожидании, пока не освободится какой-
нибудь страничный кадр. Более того, иногда запросы на выделение памяти
выдаются управляющими трактами ядра, которые уже получили монополь-
монопольный доступ к критическим ресурсам и, следовательно, не могут активизиро-
активизировать операции ввода/вывода. В нечастой ситуации, когда все запросы на вы-
выделение памяти исходят от подобных частей ядра, оно никогда не будет в со-
состоянии освободить недостающую память.
Потоки ядра kswapd благоприятно влияют на производительность системы,
потому что освобождают память в те интервалы времени, когда компьютер
все равно простаивал бы. Таким образом, процессы получают свои страницы
гораздо быстрее.
Существует отдельный поток ядра kswapd для каждого узла памяти (см. гла-
главу 8). Каждый такой поток обычно "спит" в очереди ожидания, голова кото-
которой находится в поле kswapdwait дескриптора узла. Однако если функция
aiiocpages () обнаружит, что во всех зонах, подходящих для выделения
памяти, количество свободных страничных кадров ниже некоторого "преду-
"предупреждающего" порога (значение которого зависит от значений полей
pageslow и protection дескриптора зоны памяти), то она активизирует пото-
потоки ядра kswapd соответствующих узлов памяти. Ядро приступит к утилиза-
утилизации некоторых страничных кадров, чтобы избежать гораздо более опасной
ситуации дефицита памяти.
Как было сказано в главе 8, каждый дескриптор зоны имеет поле pagesmin,
минимальное количество свободных страничных кадров, которое всегда
должно быть наготове, и поле pageshigh, "безопасное" количество свобод-
свободных страничных кадров, после достижения которого утилизацию страниц
следует прекратить.
Поток ядра kswapd вызывает функцию kswapd (). Она инициализирует поток
ядра, связывая его с центральными процессорами, которые могут обратиться
К узлу паМЯТИ. ДЛЯ ЭТОГО функция сохраняет В ПОЛе current->reclaim_state
дескриптора процесса адрес дескриптора и устанавливает флаги pfmemalloc
и pfkswap в поле current->f lags (эти флаги показывают, что процесс утили-
утилизирует память и что ему разрешено использовать всю доступную память для
его работы). Каждый раз, когда поток ядра kswapd активизируется, функция
kswapd () выполняет следующие действия:
1. Вызывает функцию f inishwait (), чтобы убрать поток ядра из очереди
kswapdwait данного узла (см. главу 3).
2. Вызывает функцию baiancepgdato, чтобы выполнить утилизацию памя-
памяти в узле потока kswapd.
3. Вызывает функцию preparetowait (), чтобы перевести процесс в состоя-
состояние task_interruptible и поместить его в очередь kswapd_wait данного
узла.
4. Вызывает функцию schedule о, чтобы предоставить центральный процес-
процессор другим выполняемым процессам.
Вызванная функция baiancepgdat () выполняет следующие основные дейст-
действия:
1. Устанавливает поля дескриптора scancontroi (см. табл. 17.2).
2. Записывает в поле temppriority каждого дескриптора зоны в узле памяти
число 12 (наименьший приоритет).
3. Выполняет цикл максимум из 13 итераций, от значения приоритета 12
до нуля, и при каждой итерации делает следующее:
• сканирует зоны памяти, чтобы найти самую верхнюю зону (от zonedma
до zonehighmem), в которой недостаточно свободных страничных кад-
ров. Каждая проверка выполняется с помощью функции zone_
watermarkok (), описанной в главе 8. Если все зоны содержат большое
количество свободных страничных кадров, функция переходит к
шагу 4;
• снова сканирует зоны памяти, от зоны zonedma до зоны, найденной на
предыдущем шаге. Для каждой зоны функция обновляет, если это не-
необходимо, поле prevpriority дескриптора зоны, записывая туда теку-
текущий приоритет, и последовательно вызывает функцию shrinkzone (),
чтобы утилизировать страницы зоны (см. разд. "Списки давно неис-
неиспользуемых страниц (LRU)"ранее в этой главе). Затем функция вызы-
вызывает функцию shrinkslab (), чтобы утилизировать страницы из сокра-
сокращаемых кэшей (см. разд. "Утилизация страниц сокращаемых кэшей
диска"ранее в этой главе)';
• если утилизировано хотя бы 32 страницы, функция прерывает цикл и
переходит к шагу 4.
4. Записывает в поле prevpriority каждого дескриптора зоны значение,
хранящееся в соответствующем поле temppriority.
5. Если остались еще зоны с дефицитом памяти, функция вызывает функцию
schedule о при условии, что флаг tifneedresched процесса установлен.
При возобновлении выполнения функция переходит к шагу 1.
Функция cache_reap()
Алгоритм утилизации страничных кадров должен также утилизировать стра-
страницы, принадлежащие slab-аллокатору (см. главу 8). При этом он полагается
на функцию cachereap (), запускаемую периодически (приблизительно раз в
две секунды) из стандартной рабочей очереди events (см. главу 4). Адрес
функции cachereapo хранится в поле func переменной reapwork типа
workstruct, имеющейся у каждого процессора.
Функция cachereap () выполняет следующие действия:
1. Пытается получить семафор cachechainsem, который защищает список
дескрипторов slab-кэшей. Если семафор уже занят, функция вызывает
ФУНКЦИЮ schedule_delayed_work(), чтобы запланировать СВОЙ следующий
вызов, и завершает работу.
2. В противном случае функция сканирует дескрипторы kmemcachet, соб-
собранные в списке cachechain. Для каждого найденного дескриптора функ-
функция выполняет следующие действия:
• если флаг slabnoreap в дескрипторе кэша установлен, значит, утили-
утилизация страничных кадров была отключена, и функция переходит к сле-
следующему кэшу в списке;
• опустошает локальный slab-кэш (см. главу 8). Эта операция может при-
привести к освобождению новых участков памяти;
• у каждого кэша есть "время уборки", хранящееся в поле nextreap
структуры kmem_iist3 внутри дескриптора кэша. Если значение jiffies
все еще меньше, чем nextreap, функция переходит к следующему кэ-
кэшу в списке;
• устанавливает следующее "время уборки", записывая в поле nextreap
значение, на четыре секунды большее, чем текущее время;
• в многопроцессорных системах функция опустошает совместно ис-
используемый slab-кэш. Эта операция может привести к освобождению
новых участков памяти;
• если в кэш недавно был добавлен новый участок памяти, т. е. если флаг
freetouched структуры kmem_iist3 внутри дескриптора кэша установ-
лен, этот кэш пропускается, и функция переходит к следующему кэшу
в списке;
• пользуясь эвристической формулой, вычисляет количество участков
памяти, подлежащих освобождению. Это значение зависит от верхнего
предела свободных объектов в кэше и от количества объектов, упако-
упакованных в один участок памяти;
• многократно вызывает функцию siabdestroyo для элементов списка
свободных участков памяти кэша, пока список не станет пустым или
пока не достигнуто целевое количество свободных участков памяти;
• вызывает функцию condreschedo для проверки флага tif_need_
resched текущего процесса и для выполнения функции schedule о, если
флаг установлен.
3. Освобождает семафор cache_chain_sem.
4. Вызывает функцию scheduie_deiayed_work(), чтобы запланировать свой
следующий вызов, и завершает работу.
Уничтожение процессов из-за нехватки памяти
Несмотря на усилия алгоритма PFRA поддерживать резерв свободных стра-
страничных кадров, давление на подсистему виртуальной памяти может стать
таким высоким, что вся доступная память будет исчерпана. Эта ситуация бы-
быстро повлечет за собой прекращение любой активности в системе: ядро будет
пытаться освободить память, чтобы удовлетворить некоторые неотложные
запросы, но у него ничего не получится, поскольку области подкачки будут
переполнены, а все кэши диска уже будут сокращены. В результате ни один
процесс не сможет продолжить выполнение и освободить, в конечном счете,
страничные кадры, которые ему принадлежат.
Чтобы справиться с такой драматической ситуацией, алгоритм утилизации
страничных кадров прибегает к помощи механизма уничтожения процессов
из-за нехватки памяти, который выбирает процесс в системе и уничтожает
его, чтобы освободить его страничные кадры. Механизм уничтожения про-
процессов действует, как хирург, ампутирующий конечность, чтобы спасти
жизнь человека. Потеря конечности ужасна, но иногда нет иного выбора.
Функция out ofmemoryo вызывается функцией aiiocpages (), когда сво-
свободной памяти становится очень мало, и алгоритму PFRA не удается утили-
утилизировать ни один страничный кадр (см. главу 8). Функция вызывает функцию
seiectbadprocess о, чтобы та выбрала жертву среди существующих про-
процессов, а затем вызывает функцию oomkiiiprocess о, уничтожающую про-
процесс.
Конечно, функция seiectbadprocess о выбирает процесс не наугад. Он
должен обладать определенными характеристиками:
□ жертва должна владеть большим количеством страничных кадров, чтобы
объем освобождаемой памяти был достаточно велик. (В качестве меры
против излишне "плодовитых" процессов функция учитывает объем памя-
памяти, занимаемой всеми потомками рассматриваемого процесса.);
□ уничтожение процесса должно привести к минимальным потерям работы;
не стоит уничтожать пакетные процессы, работающие по нескольку часов
или дней;
□ у жертвы должен быть невысокий статический приоритет, т. к. пользова-
пользователи обычно назначают низкие приоритеты процессам, не представляю-
представляющим большой ценности;
□ у жертвы не должно быть привилегий пользователя root. Такие процессы
обычно выполняют важную работу;
□ жертва не должна напрямую обращаться к аппаратным устройствам (при-
(пример — сервер X Window), потому что аппаратура может оказаться в не-
непредсказуемом состоянии;
□ жертвами не могут быть swapper (процесс 0), init (процесс 1) и ни один
другой поток ядра.
Функция seiectbadprocess о сканирует все процессы в системе и при по-
помощи некоторой эмпирической формулы и перечисленных критериев опре-
определяет значение, характеризующее очередной процесс. Функция возвращает
дескриптор процесса, наиболее подходящего для уничтожения. Затем функ-
функция outof memory () ВЫЗЫВаеТ фуНКЦИЮ oomkiiiprocess () ДЛЯ ПОСЫЛКИ СИГ-
нала на уничтожение (обычно это sigkill; см. главу 11) потомка процесса
или, если это невозможно, самого процесса. Функция oom_kiii_process()
также уничтожает все клоны, которые использует тот же дескриптор памяти,
что и выбранная жертва.
Жетон защиты от выгрузки
Как вы, вероятно, поняли из этой главы, подсистема виртуальной памяти
Linux и, особенно, алгоритм утилизации страничных кадров имеют настолько
сложный код, что очень трудно предсказать их поведение при произвольной
нагрузке на систему. Более того, возможны ситуации, в которых подсистема
виртуальной памяти проявляет патологическое поведение. Примером являет-
является так называемый феномен засорения подкачки. Когда в системе ощущается
нехватка свободной памяти, алгоритм утилизации страничных кадров энер-
энергично пытается освободить память, записывая страницы на диск и отбирая
соответствующие страничные кадры у некоторых процессов. В то же время
эти процессы стремятся продолжить выполнение и активно пытаются обра-
обратиться к своим страницам. В результате ядро выделяет для процессов стра-
страничные кадры, только что освобожденные алгоритмом PFRA, и читает их со-
содержимое с диска. Дело кончается тем, что страницы постоянно записывают-
записываются на диск и считываются с него, значительная часть времени уходит на
обращение к диску, и никакой процесс не продвигается заметно к своему за-
завершению.
Чтобы уменьшить вероятность засорения выгрузки, в версии ядра 2.6.9 была
реализована методика, предложенная Джянгом (Jiang) и Жэнгом (Zhang) в
2004 г. Так называемый жетон защиты от выгрузки присваивается единст-
единственному процессу в системе. Этот жетон освобождает процесс от утилизации
страничных кадров, позволяя ему выполняться и, возможно, завершиться да-
даже в условиях дефицита памяти.
Жетон защиты от выгрузки реализован в виде указателя swaptokenmm на де-
дескриптор памяти. Когда процесс обладает этим жетоном, указатель
swaptokenmm содержит адрес дескриптора памяти, принадлежащего про-
процессу.
Иммунитет от утилизации страничных кадров обеспечивается простым и эле-
элегантным способом. Как мы видели в разд. "Списки давно неиспользуемых
страниц (LRU)" ранее в этой главе, страница переносится из активного спи-
списка в неактивный, только если к ней не было обращений за последнее время.
Проверка выполняется функцией pagereferencedo, которая принимает во
внимание жетон защиты от выгрузки и возвращает 1 ("к странице были об-
обращения"), если страница принадлежит области памяти процесса, обладаю-
обладающего жетоном. На самом деле, есть пара случаев, в которых жетон защиты от
выгрузки не принимается во внимание: когда алгоритм утилизации странич-
страничных кадров работает ради самого обладателя жетона, а также когда алгоритм
достиг самого высокого приоритета утилизации страничных кадров (уров-
(уровня 0).
Функция grabswaptoken () определяет, должен ли жетон защиты от выгруз-
выгрузки выдаваться текущему процессу. Она вызывается при каждой серьезной
ошибке обращения к странице, а именно в двух ситуациях:
□ когда функция filemapnopageo обнаруживает, что запрошенной страни-
страницы нет в кэше (см. разд. "Выделение страниц по требованию для ото-
отображения в память"главы 16);
□ когда функция doswappage () прочитала новую страницу из области под-
подкачки (см. разд. "Загрузка выгруженных страниц" далее в этой главе).
Функция grabswaptoken () перед выдачей жетона выполняет ряд проверок.
В частности, жетон выдается, если выполнены все перечисленные условия:
□ с момента последнего вызова функции grabswaptoken () прошло не менее
двух секунд;
□ процесс-держатель жетона не возбудил серьезной ошибки обращения к
странице с момента последнего вызова функции grabswaptoken (), либо
он владеет жетоном, как минимум, в течение swaptokendefauittimeout
тиков;
□ жетон защиты от выгрузки не выдавался недавно текущему процессу.
Время владения жетоном в идеале должно быть довольно продолжительным,
доходя до нескольких минут, потому что цель выдачи жетона в том, чтобы
позволить процессу завершиться. В Linux 2.6.11 время владения жетоном по
умолчанию крайне мало — один тик. Однако системный администратор мо-
может изменить значение переменной swaptokendefauittimeout, отредакти-
отредактировав файл /proc/sys/vm/swap_token_default_timeout или сделав соответст-
соответствующий системный вызов syscti о.
Когда процесс уничтожен, ядро проверяет, был ли он держателем жетона за-
защиты от выгрузки, и, если был, освобождает жетон. Это делает функция
mmput () (см. главу 9).
Подкачка
Подкачка была введена для предоставления дисковых образов неотображен-
ным страницам. Из предшествующего обсуждения мы знаем, что есть три
типа страниц, с которыми имеет дело подсистема подкачки:
□ страницы, принадлежащие анонимной области памяти процесса (стек или
куча режима пользователя);
□ грязные страницы, принадлежащие закрытому отображению в память
файла данного процесса;
□ страницы, принадлежащие области памяти, совместно используемой при
межпроцессном взаимодействии (см. разд. "Совместно используемая па-
память IPC"главы 19).
Подобно выделению страниц по требованию, подкачка должна проходить
прозрачно для программ. Иными словами, в код программы не нужно встав-
вставлять какие-то специальные инструкции, относящиеся к подкачке. Чтобы по-
понять, как она выполняется, вспомним главу 2, где говорится, что каждая
запись Таблицы Страниц содержит флаг Present. Ядро применяет этот флаг
для сигнализации того, что страница, принадлежащая адресному пространст-
пространству процесса, была выгружена. Кроме этого флага, Linux пользуется оставши-
оставшимися битами записи Таблицы Страниц для хранения в них "идентификатора
выгруженной страницы", кодирующего местоположение выгруженной
страницы на диске. Когда возникает исключение "ошибка обращения к
странице", соответствующий обработчик исключений может распознать, что
страницы нет в оперативной памяти, и вызвать функцию, которая загрузит
недостающую страницу с диска.
Основные обязанности подсистемы подкачки могут быть сформулированы
следующим образом:
□ создавать области подкачки на диске, чтобы хранить в них страницы, не
имеющие дискового образа;
□ управлять местом в областях подкачки, выделяя и освобождая страничные
слоты по мере необходимости;
□ обеспечивать функции, выполняющие как выгрузку страниц из оператив-
оперативной памяти на диск, так и загрузку их обратно, с диска в оперативную па-
память;
□ пользоваться идентификаторами выгруженных страниц в записях Таблицы
Страниц, относящихся к выгруженным страницам, для поиска их позиций
в областях подкачки.
Резюмируя, можно сказать, что подкачка— важнейшая часть утилизации
страничных кадров. Если мы хотим быть уверены в том, что все страничные
кадры, полученные процессом (а не только те, что содержат страницы,
имеющие образ на диске), могут быть и будут утилизированы алгоритмом
PFRA, то без подкачки не обойтись. Конечно, вы можете отключить подкачку
с помощью команды swapoff, но в этом случае, однако, трешинг с повышени-
повышением нагрузки на систему произойдет быстрее.
Мы также должны заметить, что подкачкой можно пользоваться для расши-
расширения адресного пространства памяти, фактически предоставленного процес-
сам в режиме пользователя. На самом деле, большие области подкачки по-
позволяют ядру запускать несколько приложений, у которых суммарные запро-
запросы на память превышают объем физической оперативной памяти компьюте-
компьютера. Однако, с точки зрения производительности системы, симуляция опера-
оперативной памяти — не то же самое, что реальная память. Каждое обращение
процесса к странице, которая в данный момент выгружена, занимает на не-
несколько порядков больше времени, чем обращение к странице в оперативной
памяти. Короче говоря, когда производительность очень важна, к подкачке
следует прибегать лишь в самом крайнем случае; физическое увеличение
оперативной памяти по-прежнему остается лучшим решением удовлетворе-
удовлетворения растущих запросов.
Область подкачки
Страницы, выгруженные из памяти, хранятся в области подкачки, которая
может быть реализована либо в виде самостоятельного раздела на диске, ли-
либо в виде файла, включенного в какой-нибудь раздел. Можно создать не-
несколько различных областей подкачки, а их максимальное количество опре-
определяется макросом maxswapfiles и обычно равно 32.
Наличие нескольких областей подкачки позволяет системному администра-
администратору распределить пространство подкачки по нескольким дискам, чтобы обо-
оборудование могло работать с ними параллельно. Кроме того, пространство
подкачки может быть расширено во время работы, без перезагрузки системы.
Каждая область подкачки состоит из последовательности страничных сло-
слотов, блоков по 4096 байт, предназначенных для хранения выгруженных
страниц. Первый страничный слот области подкачки используется для по-
постоянного хранения некоторой информации о самой области; его формат
описывается объединением swapheader, состоящим из двух структур: info и
magic. Структура magic содержит строку, которая недвусмысленным образом
помечает часть диска как область подкачки. Она состоит только из одного
поля, magic.magic, в котором хранится 10-символьная "магическая" строка.
Структура magic позволяет ядру однозначно идентифицировать файл или
раздел как область подкачки. Текст строки, а именно "SWAPSPACE2", всегда
расположен в конце первого страничного слота.
Структура info включает в себя следующие поля:
П bootbits — не используется алгоритмом подкачки. Это поле соответствует
первым 1024 байтам области подкачки и может хранить информацию о
разделе, метку диска и т. д.;
□ version — версия алгоритма подкачки;
□ lastpage — последний фактически используемый страничный слот;
□ nrbadpages — количество дефектных страничных слотов;
□ padding [ 125 ] — ДОПОЛНЯЮЩИе байты;
□ badpages[i] — до 637 номеров, показывающих расположение дефектных
страничных слотов.
Получение и активизация области подкачки
Данные, хранящиеся в области подкачки, имеют смысл, пока работает систе-
система. Когда она отключается, все процессы уничтожаются, и данные, сохра-
сохраненные процессами в областях подкачки, пропадают. По этой причине облас-
области подкачки содержат очень мало управляющей информации, а именно тип
области и список дефектных страничных слотов. Эта управляющая информа-
информация легко умещается в одной 4-килобайтовой странице.
Обычно системный администратор создает раздел подкачки одновременно с
созданием других разделов в системе Linux, а затем пользуется командой
mkswap для превращения области на диске в новую область подкачки. Эта
команда инициализирует описанные поля у первого страничного слота. По-
Поскольку диск может содержать некоторое количество плохих блоков, про-
программа просматривает остальные страничные слоты для обнаружения де-
дефектных. Однако выполнение команды mkswap оставляет область подкачки в
неактивном состоянии. Область подкачки может быть активизирована с по-
помощью скрипта при загрузке системы или динамически, когда система уже
работает.
Любая область подкачки состоит из одного или нескольких интервалов под-
подкачки, каждый из которых представлен дескриптором swapextent. Интервал
соответствует группе страниц, а точнее, страничных слотов, физически
смежных на диске. Поэтому дескриптор содержит индекс первой страницы
интервала в области подкачки, длину интервала в страницах и номер первого
дискового сектора данного интервала. Упорядоченный список интервалов,
составляющих область подкачки, создается при активизации этой области.
Область подкачки, хранящаяся в разделе диска, состоит только из одного ин-
интервала, а область, хранящаяся в обычном файле, может состоять из несколь-
нескольких интервалов, поскольку файловая система не обязательно располагает весь
файл в последовательных блоках диска.
Как распределять страницы в областях подкачки
Выполняя выгрузку, ядро пытается сохранять страницы в последовательно
идущих слотах, чтобы минимизировать время поиска на диске при обраще-
обращении к области подкачки. Это важный момент в любом эффективном алго-
алгоритме выгрузки.
Однако, когда имеется более одной области подкачки, ситуация усложняется.
Быстрые области подкачки, т. е. те, что находятся на быстрых дисках, полу-
получают более высокий приоритет. При поиске свободного слота просмотр на-
начинается с области подкачки, имеющей наивысший приоритет. Если таких
областей несколько, области подкачки с одинаковым приоритетом выбира-
выбираются циклически, чтобы ни одна из них не переполнилась. Если в областях с
наивысшим приоритетом свободных слотов не окажется, поиск продолжается
в областях, имеющих предыдущий по высоте приоритет и т. д.
Дескриптор области подкачки
Для каждой области подкачки в памяти хранится дескриптор
swapinfostruct. Его поля перечислены в табл. 17.3.
Таблица 17.3. Поля дескриптора области подкачки
Тип Поле Описание
unsigned int flags Флаги области подкачки
spinlock_t sdev_lock Спин-блокировка, защищающая
область подкачки
struct file * swap_file Указатель на файловый объект обыч-
обычного файла или файла устройства,
в котором хранится область подкачки
struct block_device * bdev Дескриптор блочного устройства, со-
содержащего область подкачки
struct list head extentlist Голова списка интервалов, образую-
образующих область подкачки
int nrextents Количество интервалов, образующих
область подкачки
struct swap_extent * curr_swap_extent Указатель на дескриптор интервала,
использованного последним
unsigned int old_block_size Собственный размер блока у раздела,
содержащего область подкачки
unsigned short * swapmap Указатель на массив счетчиков,
по одному на каждый страничный слот
в области подкачки
unsigned int lowestjoit Страничный слот, с которого следует
начать сканирование при поиске сво-
свободного слота
unsigned int highest_bit Страничный слот, на котором следует
закончить сканирование при поиске
свободного слота
Таблица 17.3 (окончание)
Тип Поле Описание
unsigned int cluster_next Следующий страничный слот, подле-
подлежащий сканированию при поиске сво-
свободного слота
unsigned int cluster_nr Количество выделенных свободных
страничных слотов перед возобновле-
возобновлением сканирования
int prio Приоритет области подкачки
int pages Количество страничных слотов, кото-
которые могут быть использованы
unsigned long max Размер области подкачки в страницах
unsigned long inuse_pages Количество использованных странич-
страничных слотов в области подкачки
int next Указатель на следующий дескриптор
области подкачки
Поле flags содержит три перекрывающихся подполя:
□ swpused — 1, если область подкачки активна, и 0 в противном случае;
□ swpwriteok — 1, если существует возможность записи в область подкач-
подкачки; 0, если область подкачки доступна только для чтения (то есть в данный
момент она переводится в активное или неактивное состояние);
□ swpactive— это двухбитовое поле фактически является комбинацией
swpused и swpwriteok. Флаг установлен, когда оба предыдущих флага ус-
установлены.
Поле swapmap указывает на массив счетчиков, по одному на каждый стра-
страничный слот области подкачки. Если счетчик равен нулю, значит, странич-
страничный слот свободен; если счетчик положителен, значит, слот содержит выгру-
выгруженную страницу. По сути, счетчик страничного слота показывает количест-
количество процессов, совместно использующих выгруженную страницу. Если
значение счетчика равно swapmapmax (to есть 32 767), то слот содержит "по-
"постоянную" страницу, которую нельзя из него удалить. Если счетчик равен
swapmapbad C2 768), он считается дефектным и непригодным к использо-
использованию8.
8 "Постоянные" страничные слоты защищают систему от переполнения счетчиков swap_map. Без
них нормальные страничные слоты могут стать "дефектными", если к ним будет слишком много
обращений, а в результате будут потеряны данные. Впрочем, на самом деле, никто не ожидает, что
Поле prio содержит целое со знаком, определяющее порядок, в котором под-
подсистема подкачки должна перебирать области подкачки.
Поле sdeviock является спин-блокировкой, которая защищает структуры об-
области подкачки, в основном, дескриптор, от параллельного обращения в мно-
многопроцессорных системах.
Массив swapinfo содержит maxswapfiles дескрипторов областей подкачки.
Используются только области, у которых установлены флаги swpused, пото-
потому что эти области активны. На рис. 17.6 изображены массив swapinfo, одна
область подкачки и соответствующий массив счетчиков.
Рис. 17.6. Структуры области подкачки
Переменная nrswapf iies хранит индекс последнего элемента массива, кото-
который содержит или содержал использованный дескриптор области подкачки.
Несмотря на свое название ("количество файлов подкачки"), переменная не
содержит количество активных областей подкачки.
Дескрипторы активных областей подкачки также занесены в список, отсор-
отсортированный по приоритетам областей. Список реализован с помощью поля
next дескриптора области подкачки, которое содержит индекс следующего
дескриптора в массиве swapinfo. Такое использование поля в качестве ин-
индекса массива нетипично, поскольку большинство полей с именем next со-
содержит указатели.
какой-нибудь счетчик страничного слота достигнет значения 32 768. Здесь имеет место определен-
определенная перестраховка.
Переменная swapiist, имеющая тип swapiistt, состоит из следующих
полей:
□ head — индекс первого элемента списка в массиве swapinf о;
□ next — индекс в массиве swapinf о дескриптора следующей области под-
подкачки, которая должна быть выбрана для выгрузки страниц. Это поле ис-
используется для реализации алгоритма циклического обхода областей под-
подкачки с максимальным приоритетом при поиске свободных слотов.
Спин-блокировка swapiock защищает список от параллельного обращения
в многопроцессорных системах.
Поле max дескриптора области подкачки содержит размер области в страни-
страницах, а поле pages содержит количество страничных слотов, которые могут
быть использованы. Эти два числа различны, потому что в поле pages не учи-
учитываются первый страничный слот и дефектные слоты.
Наконец, переменная nrswappages содержит количество доступных (сво-
(свободных и не дефектных) страничных слотов во всех активных областях под-
подкачки, а переменная totaiswappages — суммарное количество не дефектных
страничных слотов.
Идентификатор выгруженной страницы
Выгруженная страница естественным и уникальным образом идентифициру-
идентифицируется с помощью индекса области подкачки в массиве swapinfo и индекса
страничного слота внутри области подкачки. Поскольку первая (с индек-
индексом 0) страница области подкачки зарезервирована для объединения
swapheader, описанного ранее, индекс первой полезной страницы равен 1.
Формат идентификатора выгруженной страницы изображен на рис. 17.7.
Рис. 17.7. Идентификатор выгруженной страницы
Функция swpentry (type, offset) строит идентификатор выгруженной стра-
страницы по индексу области подкачки type и индексу страничного слота offset.
В противоположность ей, функции swptype и swpof f set извлекают из иден-
идентификатора выгруженной страницы индекс области подкачки и индекс стра-
страничного слота соответственно.
Когда страница выгружается на диск, ее идентификатор заносится в Таблицу
Страниц, чтобы страницу можно было найти, когда потребуется. Обратите
внимание, что младший бит такого идентификатора, соответствующий флагу
Present, всегда сброшен, чтобы отметить тот факт, что страница не находится
в оперативной памяти. Тем не менее, хотя бы один из остальных 31 битов
должен быть равен единице, поскольку нет страниц, хранящихся в слоте но-
номер 0 области подкачки номер 0. Следовательно, можно идентифицировать
три разные ситуации на основании значения записи в Таблице Страниц:
□ Запись содержит null — страница не принадлежит адресному пространст-
пространству процесса, либо соответствующий страничный кадр еще не был назна-
назначен процессу (выделение страниц по требованию).
□ Не все из старших 31 битов равны нулю, а младший бит равен нулю —
страница выгружена.
□ Младший бит равен 1 — страница находится в оперативной памяти.
Максимальный размер области подкачки определяется количеством битов,
доступных для идентификации слота. В архитектуре 80x86 двадцать четыре
бита позволяют области подкачки иметь размер до 224 слотов F4 Гбайт).
Поскольку страница может принадлежать адресным пространствам несколь-
нескольких процессов, она может быть выгружена из адресного пространства одного
процесса, но оставаться в оперативной памяти. Следовательно, имеется воз-
возможность выгрузить одну страницу несколько раз. Конечно, страница физи-
физически выгружается и сохраняется на диске только один раз, но каждая после-
последующая попытка выгрузить ее увеличивает счетчик swapmap.
Функция swapdupiicateo обычно вызывается при попытке выгрузить уже
выгруженную страницу. Она просто проверяет корректность идентификатора
выгруженной страницы, переданного ей в качестве параметра, и увеличивает
соответствующий счетчик swapmap. Более подробно она выполняет следую-
следующие действия:
1. Вызывает функции swp_type и swpoffset, чтобы извлечь из аргумента но-
номер области подкачки и индекс страничного слота.
2. Проверяет, активна ли идентифицированная область. Если нет, возвраща-
возвращает 0 (недопустимый идентификатор).
3. Проверяет допустимость страничного слота, и свободен ли он (счетчик
swapmap должен быть больше 0 и меньше swapmapbad). Если нет, воз-
возвращает 0 (недопустимый идентификатор).
4. Если функция на этом шаге, значит, идентификатор выгруженной страни-
страницы определяет допустимую страницу. Функция увеличивает счетчик
swapmap страничного слота, если он еще не достиг значения swapmapmax.
5. Возвращает 1 (корректный идентификатор).
Перевод области подкачки
в активное и неактивное состояние
После инициализации области подкачки суперпользователь (или, точнее, лю-
любой пользователь с capsysadmin; см. разд. "Права и возможности процесса"
главы 20) может запускать программы swapon и swapoff, чтобы активизиро-
активизировать область подкачки или перевести ее в неактивное состояние, соответст-
соответственно. Эти программы пользуются системными вызовами swapon о и
swapoff (), и мы кратко опишем их служебные процедуры.
Служебная процедура sys_swapon()
Служебная процедура sys swapon () принимает следующие параметры:
□ speciaif ile — этот параметр указывает на путь (в адресном пространстве
режима пользователя) к файлу устройства (разделу) или к обычному фай-
файлу, который реализует область подкачки;
П swapf lags ЭТОТ Параметр СОСТОИТ ИЗ ОДНОГО бита SWAPFLAGPREFER И
31 бита приоритета области подкачки (эти биты имеют смысл, только если
бит SWAP_FLAG_PREFER установлен).
Эта функция проверяет поля объединения swapheader, которое было записа-
записано в первый слот при создании области подкачки. Функция выполняет сле-
следующие действия:
1. Проверяет, есть ли у текущего процесса возможность capsysadmin.
2. Просматривает первые nrswapfiies элементов массива дескрипторов об-
областей swapinfo в поисках первого дескриптора со сброшенным флагом
swpused, показывающим, что соответствующая область подкачки неак-
неактивна. Если неактивная область найдена, функция переходит к шагу 4.
3. Индекс новой области подкачки равен nrswapfiies. Функция убеждается,
что количество битов, зарезервированных под индекс области подкачки,
достаточно для кодирования нового индекса. Если это не так, функция
возвращает код ошибки; в противном случае она увеличивает на единицу
значение nr_swapf iles.
4. Определен индекс неиспользуемой области подкачки. Функция инициали-
инициализирует поля дескриптора. В частности, она записывает в поле flags значе-
значение SWPJJSED, а В ПОЛЯ lowest_bit И highest_bit — нули.
5. Если параметр swapfiags задает приоритет для новой области подкачки,
функция устанавливает поле prio дескриптора. В противном случае, она
записывает в поле значение, на единицу меньшее самого низкого приори-
приоритета среди активных областей подкачки (исходя из предположения, что
последняя активизированная область подкачки находится на самом мед-
медленном блочном устройстве). Если других активных областей подкачки
еще нет, функция записывает -1.
6. Копирует строку, на которую указывает параметр special file, из адрес-
адресного пространства режима пользователя.
7. Вызывает функцию filpopeno, чтобы открыть файл, заданный парамет-
параметром speciaifile (см. разд. "Системный вызов ореп()"главы 12).
8. Сохраняет адрес файлового объекта, возвращенного функцией
f iipopen (), в поле swapf ile дескриптора области подкачки.
9. Убеждается, что область подкачки еще не активна, для чего просматрива-
просматривает другие активные области подкачки в массиве swapinfo. Это делается
путем проверки адресов объектов address_space, хранящихся в полях
swapf iie->f_mapping дескрипторов областей подкачки. Если оказывается,
что область подкачки уже активна, функция возвращает код ошибки.
10. Если параметр speciaifile идентифицирует файл блочного устройства,
функция выполняет следующие действия:
• вызывает функцию bdciaimo, чтобы сделать подсистему подкачки
держателем блочного устройства (см. разд. "Блочные устройства"
главы 14). Если у блочного устройства уже есть держатель, функция
возвращает код ошибки;
• сохраняет адрес дескриптора biockdevice в поле bdev дескриптора об-
области подкачки;
• сохраняет текущий размер блока устройства в поле oidbiocksize де-
дескриптора области подкачки, а затем устанавливает размер блока уст-
устройства в значение 4096 байтов (размер страницы).
И. Если параметр speciaifile идентифицирует обычный файл, функция вы-
выполняет следующие действия:
• проверяет флаг sswapfile в поле ifiags индексного дескриптора
файла. Если этот флаг установлен, функция возвращает код ошибки,
потому что файл уже используется в качестве области подкачки;
• сохраняет адрес дескриптора блочного устройства, содержащего файл,
в поле bdev дескриптора области подкачки.
12. Читает дескриптор swapheader, расположенный в слоте 0 области под-
подкачки. С этой целью вызывает функцию readcachepage (), передавая ей
в качестве параметров объект addressspace, на который указывает поле
swap_fiie->f_mapping, индекс страницы 0, адрес файлового метода
readpage (хранящийся В поле swap_file->f_mapping->a_ops->readpage) И
указатель на файловый объект swapf ile. Затем функция ждет, пока стра-
страница не будет прочитана в память.
13. Убеждается, что магическая строка в последних 10 байтах первой стра-
страницы равна MSWAPSPACE2M. Если это не так, возвращает код ошибки.
14. Инициализирует поля lowestbit и highestbit дескриптора области под-
подкачки в соответствии с размером области, хранящимся в поле
info. last_page объединения swap_header.
15. Вызывает функцию vmaiioco, чтобы создать массив счетчиков, ассоции-
ассоциированных с новой областью подкачки, и сохраняет его адрес в поле
swapmap дескриптора области подкачки. Затем инициализирует элементы
массива нулем или значением swap_map_bad согласно списку дефектных
страничных слотов, который хранится в поле info.badpages объединения
swap_header.
16. Вычисляет количество полезных страничных слотов по значениям полей
info.lastpage И info.nrbadpages В перВОМ Страничном СЛОТе И СОХраня-
ет это значение в поле pages дескриптора области подкачки. Кроме того,
записывает в поле max суммарное количество страниц в области под-
подкачки.
17. Строит список интервалов подкачки extentiist для новой области
подкачки (только один, если областью подкачки является раздел дис-
диска) и соответствующим образом устанавливает поля nrextents и
currswapextent дескриптора области подкачки.
18. Записывает в поле flags дескриптора области подкачки значение
SWP_ACTIVE.
19. Обновляет глобальные переменные nr_good_pages, nr_swap_pages И
total_swap_pages.
20. Заносит дескриптор области подкачки в список, на который указывает
переменная swapiist.
21. Возвращает 0 (успешное завершение).
Служебная процедура sys_swapoff()
Служебная процедура sysswapof f () делает не активной область подкачки,
идентифицируемую параметром speciaif ile. Она гораздо сложнее и требует
больше времени, чем служебная процедура sysswapon (), потому что раздел,
переводимый в неактивное состояние, может содержать страницы, принад-
принадлежащие нескольким процессам. Функции приходится сканировать область
подкачки и загружать в память все имеющиеся страницы. Поскольку каждая
загрузка требует выделения нового страничного кадра, вся операция может
закончиться неудачей, если свободных страничных кадров не окажется.
В таком случае функция возвращает код ошибки. Для достижения своих це-
целей функция выполняет следующие действия:
1. Проверяет, есть ли у текущего процесса возможность capsysadmin.
2. Копирует строку, на которую указывает параметр speciaif He, в простран-
пространство ядра.
3. Вызывает функцию filpopeno, чтобы открыть файл, на который ссыла-
ссылается параметр special file. Как обычно, эта функция возвращает адрес
файлового объекта.
4. Сканирует список swapiist дескриптора области подкачки и сравнивает
адрес файлового объекта, возвращенного функцией filpopeno, с адреса-
адресами в полях swapfile дескрипторов активных областей подкачки. Если
совпадение не обнаружено, значит, функции был передан некорректный
параметр, и она возвращает код ошибки.
5. Вызывает функцию capvmenoughmemory () для проверки, достаточно ли
имеется свободных страничных кадров, чтобы загрузить все страницы,
хранящиеся в области подкачки. Если их недостаточно, область подкачки
не может быть переведена в неактивное состояние. Функция освобождает
файловый объект и возвращает код ошибки. Это лишь приблизительная
оценка, но она избавляет ядро от выполнения бесполезных операций с
ДИСКОМ. При ВЫПОЛНеНИИ ЭТОЙ Проверки фуНКЦИЯ capvmenoughmemory ()
учитывает страничные кадры, выделенные через slab-кэши, у которых
флаг slabreclaimaccount установлен (см. главу8). Количество таких
страниц, считающихся утилизируемыми, хранится в переменной
slab_reclaim_pages.
6. Удаляет дескриптор области подкачки из списка swapiist.
7. Обновляет переменные nrswappages и totaiswappages, вычитая значе-
ние поля pages дескриптора области подкачки.
8. Сбрасывает флаг swpwriteok в поле flags дескриптора области подкачки,
тем самым запрещая алгоритму утилизации страничных кадров дальней-
дальнейшую выгрузку страниц в эту область.
9. Вызывает функцию trytounuseo, чтобы принудительно загрузить в
оперативную память все страницы, оставшиеся в области подкачки и со-
соответствующим образом обновить Таблицы Страниц процессов, обра-
обращающихся к этим страницам. Во время выполнения этой функции у теку-
текущего процесса, выдавшего команду swapoff, установлен флаг pfswapoff.
Установка этого флага может иметь лишь один результат: в случае острой
нехватки памяти функция seiectbadprocess о неизбежно выберет и
уничтожит этот процесс! (см. разд. "Уничтожение процессов из-за не-
нехватки памяти"ранее в этой главе).
10. Ждет, пока драйвер блочного устройства, которое содержит область под-
подкачки, не будет откупорен (см. разд. "Активизация драйвера блочного
устройства" главы 14). Таким образом, запросы на чтение, выданные
функцией trytounuseO, будут обработаны драйвером до перевода об-
области подкачки в неактивное состояние.
11. Если функции trytounuseO не удастся выделить все запрошенные
страничные кадры, область подкачки нельзя будет перевести в неактив-
неактивное состояние. Тогда описываемая функция выполнит следующие дейст-
действия:
• вернет дескриптор области подкачки в список swapiist и установит
его поле flags в значение swp_writeok;
• восстановит оригинальные значения переменных nrswappages и
totaiswappages, прибавив к ним значение поля pages дескриптора
области подкачки;
• вызывает функцию filpcioseO, чтобы закрыть файл, открытый на
шаге 3 (см. разд. "Системный вызов closeQ" главы 12), и возвращает
код ошибки.
12. В противном случае содержимое всех использованных страничных сло-
слотов было успешно перенесено в оперативную память. Функция выполня-
выполняет следующие действия:
• освобождает области памяти, использованные для хранения массива
swapmap и дескрипторов интервалов подкачки;
• если область подкачки находится в разделе диска, функция восстанав-
восстанавливает оригинальное значение размера блока, которое было сохранено
в поле oidbiocksize дескриптора области подкачки. Кроме того,
функция вызывает функцию bdreiease (), чтобы подсистема подкачки
больше не являлась держателем блочного устройства;
• если область подкачки находится в обычном файле, функция сбрасы-
сбрасывает флаг sswapfile в индексном дескрипторе файла;
• дважды вызывает функцию filpcioseO. Первый раз для файлового
объекта swapf ile, а второй — для объекта, возвращенного функцией
f iip_open () на шаге 3;
• возвращает 0 (успешное завершение).
Функция try_to_unuse()
Функция trytounuse () принимает в качестве параметра индекс, идентифи-
идентифицирующий область подкачки, подлежащую переводу в неактивное состояние.
Она загружает выгруженные страницы и обновляет все Таблицы Страниц,
которые выгружали страницы в эту область. С этой целью функция просмат-
просматривает адресные пространства всех потоков ядра и процессов, начиная с де-
дескриптора памяти initmm, используемого в качестве маркера. Эта функция
выполняется очень долго, как правило, с разрешенными прерываниями. По-
Поэтому очень важна синхронизация с другими процессами.
Функция try_to_unuse() перебирает элементы массива swapmap области под-
подкачки. Когда она находит используемый страничный слот, она вначале за-
загружает страницу в память, а затем начинает поиск процессов, ссылающихся
на эту страницу. Порядок этих двух операций исключительно важен для из-
избежания конфликтов одновременного обращения. Пока выполняется опера-
операция ввода/вывода, страница остается заблокированной, и ни один процесс не
может к ней обратиться. Когда ввод/вывод закончен, страница снова блоки-
блокируется функцией try_to_unuse(), чтобы ее не мог выгрузить какой-нибудь
управляющий тракт ядра. Конфликта удается избежать, поскольку каждый
процесс просматривает кэш страниц перед запуском операции выгрузки или
загрузки (см. разд. "Кэш подкачки" далее в этой главе). Наконец, функция
trytounuseO помечает рассматриваемую область подкачки как область, в
которую запрещена запись (флаг swpwriteok сброшен), и ни один процесс не
может выполнить выгрузку в страничные слоты этой области.
Впрочем, функция trytounuseO может быть вынуждена несколько раз пе-
перебрать элементы массива swapmap, содержащего счетчики области подкач-
подкачки. Это может произойти, если какие-то области памяти, хранящие ссылки на
выгруженные страницы, исчезнут во время одного перебора элементов, а
впоследствии снова появятся в списках процессов.
Вспомним, например, описание функции domunmap () (см. главу 9). Как толь-
только какой-нибудь процесс освобождает интервал линейных адресов, функция
do munmap () удаляет из списка процесса все области памяти, включавшие ли-
линейные адреса, затронутые в этой операции. Впоследствии функция снова
заносит в список процесса области памяти, которые были освобождены лишь
частично. Функция domunmapO отвечает за освобождение выгруженных
страниц, принадлежащих интервалу освобожденных линейных адресов. Она
достойна похвалы за то, что не освобождает выгруженные страницы, принад-
принадлежащие областям памяти, которые должны будут вернуться в список про-
процесса.
Итак, функция try to unuse (), возможно, не сумеет найти процесс, ссылаю-
ссылающийся на данный страничный слот из-за того, что соответствующая область
памяти будет временно отсутствовать в списке процесса. Чтобы справиться с
этой ситуацией, функция trytounuse () продолжает перебор элементов мас-
массива swapmap, пока все счетчики ссылок не примут нулевые значения. В кон-
це концов, "призрачные" области памяти, на которые ссылаются выгружен-
выгруженные страницы, снова появятся в списках процессов, и функция trytounuse ()
успешно освободит все страничные слоты.
Теперь мы опишем основные действия, выполняемые функцией try_to_
unuse (). Она входит в цикл по счетчикам ссылок в массиве swapmap области
подкачки, полученной через параметр. Этот цикл прерывается, и функция
возвращает код ошибки, если текущий процесс получает сигнал. Для каждого
счетчика ссылок функция выполняет следующие действия:
1. Если счетчик равен нулю (нет страниц) или значению swapmapbad, функ-
функция переходит к следующему страничному слоту.
2. В прОТИВНОМ Случае Она вызывает функцию read_swap_cache_async() (см.
разд. "Загрузка выгруженных страниц" далее в этой главе), чтобы загру-
загрузить страницу в память. Операция загрузки состоит из выделения нового
страничного кадра, если это необходимо, и заполнения его данными из
страничного слота и занесения страницы в кэш страниц.
3. Затем функция ждет, пока новая страница не будет корректно заполнена
данными с диска, и блокирует ее.
4. Пока функция находилась на предыдущем шаге, выполнение процесса
могло быть приостановлено. Поэтому функция снова проверяет, равен ли
нулю счетчик ссылок страничного слота. Если равен, значит, эта страница
была освобождена другим управляющим трактом ядра, и функция перехо-
переходит к следующему страничному слоту.
5. Вызывает функцию unusej?rocess() для каждого дескриптора памяти
в двунаправленном списке, голова которого находится в поле initmm
(см. разд. "Дескриптор памяти" главы 9). Эта функция, требующая очень
много времени, сканирует все записи Таблицы Страниц процесса, вла-
владеющего данным дескриптором памяти, и заменяет каждое вхождение
идентификатора выгруженной страницы на физический адрес страничного
кадра. Для отражения этих действий функция также уменьшает счетчик
страничного слота в массиве swapmap (если он не равен swapmapmax) и
увеличивает счетчик ссылок страничного кадра.
6. Вызывает функцию shmemunuse(), чтобы проверить, является ли выгру-
выгруженная страница совместно используемой при межпроцессном взаимо-
взаимодействии, и корректно обработать эту ситуацию (см. разд. "Совместно ис-
используемая память межпроцессного взаимодействия" главы 19).
7. Проверяет значение счетчика ссылок страницы. Если оно равно swap_map_
мах, значит, страничный слот является "постоянным". Чтобы освободить
его, функция своей властью записывает единицу в счетчик ссылок.
8. Кэш подкачки тоже может владеть страницей (в этом случае он участво-
участвовал в увеличении счетчика ссылок). Если страница принадлежит кэшу
подкачки, функция вызывает функцию swapwritepage () для сброса со-
содержимого страницы на диск (если страница "грязная") и функцию
delete_from_swap_cache() ДЛЯ удаления Страницы ИЗ КЭШа ПОДКачкИ И
уменьшения ее счетчика ссылок.
9. Устанавливает флаг PGdirty дескриптора страницы, снимает блокировку
со страничного кадра и уменьшает его счетчик ссылок (чтобы компенси-
компенсировать увеличение, выполненное на шаге 5).
10. Проверяет флаг tifneedresched текущего процесса. Если он установлен,
вызывает функцию schedule о, чтобы освободить процессор. Перевод
области подкачки в неактивное состояние является долгим делом, и ядро
должно позволить другим процессам в системе выполняться, как им нуж-
нужно. С этого шага функция trytounuse () продолжит свое выполнение по-
после того, как данный процесс снова будет выбран планировщиком.
11. Переходит к шагу 1 и рассматривает следующий страничный слот.
Функция продолжает работу, пока каждый счетчик ссылок в массиве
swapmap не примет нулевое значение. Вспомним, что даже если функция
приступит к рассмотрению следующего страничного слота, счетчик ссылок
предыдущего слота может остаться положительным. Дело в том, что "при-
"призрачный" процесс может по-прежнему ссылаться на страницу, чаще всего из-
за того, что некоторые области памяти были временно удалены из списка
процесса, просмотренного на шаге 5. В конце концов, функция try_to_
unuse () "отловит" все ссылки. Тем временем, однако, страница уже не нахо-
находится в кэше подкачки, она разблокирована, а ее копия все еще хранится в
страничном слоте области подкачки, переводимой в неактивное состояние.
Можно ожидать, что такая ситуация приведет к потере данных. Например,
предположим, что некоторый призрачный процесс обращается к страничному
слоту и начинает загружать выгруженную страницу. Поскольку страницы
больше нет в кэше подкачки, процесс заполняет новый страничный кадр дан-
данными, прочитанными с диска. Однако этот страничный кадр будет отличать-
отличаться от страничных кадров, принадлежащих процессам, которые, как предпола-
предполагается, используют эту страницу совместно с призрачным процессом.
Эта проблема не возникает при переводе области подкачки в неактивное со-
состояние, потому что вмешательство со стороны призрачного процесса может
произойти, только если выгруженная страница принадлежит закрытому ано-
анонимному отображению в память9. В таком случае страничный кадр обраба-
9 Фактически страница может также принадлежать области памяти, совместно используемой при
межпроцессном взаимодействии.
тывается с помощью механизма "копирования при записи", описанного в гла-
главе 9, и поэтому вполне допустимо присваивать различные страничные кадры
процессам, ссылающимся на страницу. Однако функция trytounuse () по-
помечает страницу как грязную (шаг 9), иначе функция shrinkiisto может
впоследствии удалить страницу из Таблицы Страниц какого-нибудь процес-
процесса, не сохранив ее в другой области подкачки (см. разд. "Выгрузка страниц4
далее в этой главе).
Выделение и освобождение страничного слота
Как мы увидим позже, при освобождении памяти ядро выгружает много
страниц на короткий период времени. Поэтому важно постараться сохранить
их в последовательно идущих слотах, чтобы минимизировать время поиска
на диске при обращении к области подкачки.
При разработке алгоритма, который ищет свободный слот, первый пришед-
пришедший на ум подход заключается в выборе между двумя простыми и довольно
экстремальными стратегиями:
□ всегда начинать с начала области подкачки. В результате может увели-
увеличиться среднее время поиска во время операций выгрузки, потому что
свободные страничные слоты могут оказаться разбросанными далеко друг
от друга;
□ всегда начинать с последнего выделенного страничного слота. В результа-
результате может увеличиться среднее время поиска во время операций загрузки
выгруженных страниц, если область подкачки, в основном, пуста (а так
оно обычно и бывает), потому что немногочисленные занятые страничные
слоты могут оказаться разбросанными далеко друг от друга.
В Linux принят гибридный подход. Алгоритм всегда начинает с последнего
выделенного страничного слота, если не возникает одна из следующих си-
ситуаций:
□ достигнут конец области подкачки;
□ после последнего рестарта с начала области подкачки было выделено
swapfilecluster свободных страничных слотов (обычно 256).
Поле ciusternr в дескрипторе swapinfostruct содержит количество выде-
выделенных свободных страничных слотов. Это поле сбрасывается в ноль, когда
функция заново приступает к выделению с начала области подкачки. Поле
clusternext содержит индекс первого страничного слота, который нужно
рассмотреть при следующем выделении10.
10 Как вы, вероятно, заметили, структуры данных в Linux не всегда названы подходящим образом.
В данном случае ядро на самом деле не собирает в "кластер" страничные слоты области подкачки.
Чтобы ускорить процесс поиска свободных страничных слотов, ядро поддер-
поддерживает в полях lowestbit и highestbit каждого дескриптора области под-
подкачки самую свежую информацию. Эти поля определяют первый и послед-
последний страничные слоты, которые могут быть свободными. Иными словами,
каждый страничный слот ниже lowestbit и выше highestbit наверняка
занят.
Функция scan_swap_map()
Функция scanswapmapo применяется для поиска свободного страничного
слота в данной области подкачки. Она принимает один параметр, который
указывает на дескриптор области подкачки, а возвращает индекс свободного
страничного слота. Если область подкачки не содержит свободных странич-
страничных слотов, возвращается 0. Функция выполняет следующие действия:
1. Вначале пытается воспользоваться текущим кластером. Если значение по-
поля clusternr дескриптора области подкачки положительно, функция пе-
перебирает элементы массива счетчиков swapmap, начиная с элемента с ин-
индексом clusternext, и ищет нулевой счетчик. Если функция его находит,
она уменьшает значение в поле clusternr и переходит к шагу 4.
2. Если функция на этом шаге, значит, либо поле clusternr содержит ноль,
либо поиск, начавшийся с индекса clusternext в массиве swapmap, не дал
нулевой элемент. Нужно переходить ко второму этапу гибридного поиска.
Функция инициализирует поле clusternr значением swapfilecluster и
возобновляет перебор элементов массива, начиная с индекса lowestbit,
пытаясь найти группу из swapfilecluster свободных страничных слотов.
Если такую группу найти удается, функция переходит к шагу 4.
3. Группы из swapfilecluster свободных страничных слотов не существует.
Функция возобновляет перебор элементов массива, начиная с индекса
lowestbit, пытаясь найти один свободный страничный слот. Если ей это
не удается, она записывает в поле lowestbit максимальный индекс в мас-
массиве, а в поле highestbit — ноль, после чего возвращает 0 (область под-
подкачки заполнена).
4. Элемент с нулевым значением найден. Функция записывает в него едини-
единицу, уменьшает значение nr_swap_pages, обновляет ПОЛЯ lowestbit И
highestbit, еСЛИ необходимо, увеличивает на единицу ПОЛе inuse_pages И
записывает в поле clusternext индекс только что выделенного странич-
страничного слота плюс 1.
5. Возвращает индекс выделенного страничного слота.
Функция get_swap_page()
Функция getswap page () применяется для поиска свободного страничного
слота путем просмотра всех активных областей подкачки. Она возвращает
идентификатор выгруженной страницы в выделенном страничном слоте, ли-
либо ноль, если все области подкачки заполнены. При этом функция учитывает
различные приоритеты активных областей подкачки.
Функция выполняет два прохода, чтобы минимизировать время работы, когда
найти страничный слот легко. Первый проход является неполным и затраги-
затрагивает только области с одинаковым приоритетом: функция просматривает их
по кругу в поисках свободного слота. Если свободного страничного слота не
оказывается, функция выполняет второй проход, начав с начала списка об-
областей подкачки. На этом проходе проверяются все области. Говоря более
конкретно, функция выполняет следующие действия:
1. Если поле nrswappages содержит ноль, или активных областей подкачки
нет, функция возвращает 0.
2. Начинает с просмотра области подкачки, на которую указывает
swapiist.next (вспомним, что список областей подкачки отсортирован по
убыванию приоритетов).
3. Если данная область подкачки активна, функция вызывает функцию
scanswapmap () для выделения свободного страничного слота. Если функ-
функция scanswapmapO возвращает индекс страничного слота, то, в принципе,
все сделано. Однако описываемая функция должна еще подготовиться к
своему следующему вызову. Поэтому она обновляет swapiist.next так,
чтобы оно указывало на следующую область в списке областей подкачки,
если последняя имеет тот же приоритет (тем самым продолжается цикли-
циклическое использование этих областей подкачки). Если же следующая об-
область подкачки имеет не тот приоритет, что данная, то функция записыва-
записывает в swapiist.next указатель на первую область в списке (чтобы следую-
следующий поиск начался с областей подкачки, имеющих наивысший
приоритет). Функция заканчивает работу, возвращая идентификатор вы-
выгруженной области в соответствии с только что выделенным страничным
слотом.
4. Либо запись в область подкачки запрещена, либо в ней нет свободных
страничных слотов. Если следующая область в списке областей подкачки
имеет тот же приоритет, что и текущая, функция делает ее текущей и пе-
переходит к шагу 3.
5. На этом шаге следующая область в списке областей подкачки имеет при-
приоритет, меньший, чем у предыдущей. Дальнейшие действия функции за-
зависят от того, какой из двух проходов она выполняет.
• если это первый (неполный) проход, функция рассматривает первую
область подкачки в списке и переходит к шагу 3, тем самым начиная
второй проход;
• в противном случае она проверяет наличие следующего элемента в
списке. Если таковой имеется, она рассматривает его и переходит к
шагу 3.
6. К этому шагу список полностью просмотрен на втором проходе, и свобод-
свободные страничные слоты найдены не были. Функция возвращает 0.
Функция swap_free()
Функция swapf гее () вызывается во время загрузки выгруженной страницы,
чтобы уменьшить соответствующий счетчик swapmap (см. табл. 17.3). Когда
счетчик достигает нуля, страничный слот становится свободным, поскольку
его идентификатор больше не содержится ни в одной записи Таблицы Стра-
Страниц. Однако в разд. "Кэш подкачки" далее в этой главе мы увидим, что кэш
подкачки учитывается при подсчете количества владельцев страничного
слота.
Функция принимает единственный параметр entry, задающий идентификатор
выгруженной страницы, и выполняет следующие действия:
1. Извлекает индекс области подкачки и индекс страничного слота offset из
параметра entry и получает адрес дескриптора области подкачки.
2. Проверяет, активна ли область подкачки, и немедленно возвращает управ-
управление, если неактивна.
3. Если значение счетчика swapmap, соответствующего освобождаемому
страничному слоту, меньше, чем swap_map_max, функция уменьшает его.
Вспомним, что счетчики со значением swapmapmax считаются постоян-
постоянными (не удаляемыми).
4. Если счетчик swapmap достиг нулевого значения, функция увеличивает
значение nrswappages, уменьшает значение поля inusepages и обновля-
обновляет, если необходимо, поля lowestbit и highestbit дескриптора области
подкачки.
Кэш подкачки
Перенос страниц в область подкачки и обратно является операцией, чреватой
многочисленными конфликтами одновременного обращения. В частности,
подсистема подкачки должна аккуратно обрабатывать следующие случаи:
□ Множественные загрузки выгруженной страницы— два процесса могут
одновременно пытаться загрузить одну и ту же совместно используемую
анонимную страницу.
□ Параллельные загрузки и выгрузки — процесс может пытаться загрузить
страницу, выгружаемую в этот момент алгоритмом утилизации странич-
страничных кадров.
Для решения этих проблем синхронизации был введен кэш подкачки. Ключе-
Ключевое правило гласит, что никто не может начать выгрузку или загрузку, не
проверив, содержит ли кэш страницу, затрагиваемую этой операцией. Благо-
Благодаря кэшу подкачки параллельные операции над одной страницей всегда дей-
действуют на один и тот же страничный кадр. Следовательно, ядро может пола-
полагаться на флаг PGiocked дескриптора страницы, чтобы избежать конфликтов
одновременного обращения.
Рассмотрим, например, два процесса, использующих одну выгруженную
страницу. Когда первый процесс пытается к ней обратиться, ядро запускает
операцию загрузки. Самый первый шаг состоит из проверки наличия стра-
страничного кадра в кэше подкачки. Предположим, его там нет. Тогда ядро выде-
выделяет новый страничный кадр и заносит его в кэш. Затем оно запускает опера-
операцию чтения содержимого страницы из области подкачки. Тем временем вто-
второй процесс обращается к этой совместно используемой анонимной
странице. Как и для первого процесса, ядро запускает операцию загрузки и
проверяет, находится ли нужный страничный кадр в кэше подкачки. Сейчас
он уже в кэше, и ядро просто обращается к дескриптору этого страничного
кадра и приостанавливает текущий процесс, пока флаг PGiocked не будет
сброшен, т. е. пока не закончится чтение данных.
Кэш подкачки также играет исключительно важную роль, когда имеют место
параллельные операции загрузки и выгрузки. Как было сказано в
разд. "Утилизация при дефиците памяти" ранее в этой главе, функция
shrinkiist о начинает выгрузку анонимной страницы, только если функция
trytounmap () успешно удалила страничный кадр из Таблиц Страниц всех
процессов режима пользователя, владеющих этой страницей. Однако один из
этих процессов может обратиться к странице, что приведет к запуску опера-
операции загрузки, в то время как операция выгрузки (то есть записи) еще не за-
завершилась.
Перед тем как она будет записана на диск, каждая страница, подлежащая вы-
выгрузке, сохраняется в кэше подкачки функцией shrinkiisto. Рассмотрим
страницу Р, совместно используемую двумя процессами: А и В. Изначально
записи Таблицы Страниц обоих процессов содержат ссылки на страничный
кадр, а у страницы два владельца. Этот случай иллюстрируется рис. 17.8, а.
Когда алгоритм PFRA выбирает страницу для утилизации, функция
shrinkiist о заносит страничный кадр в кэш подкачки. Как показано на
рис. 17.8,6, теперь у страничного кадра три владельца, в то время как на
страничный слот в области подкачки ссылается только кэш подкачки. Затем
алгоритм PFRA вызывает функцию trytounmapo, чтобы удалить ссылки на
страничный кадр из Таблицы Страниц процессов. Когда эта функция завер-
завершит работу, на страничный кадр будет ссылаться только кэш подкачки, а на
страничный слот— оба процесса и кэш подкачки, что показано на
рис. 17.8, в. Предположим, что пока содержимое страницы записывалось на
диск, процесс В обратился к ней, т. е. попытался обратиться к ячейке памяти,
пользуясь линейным адресом в пределах этой страницы. Тогда обработчик
события "ошибка обращения к странице" найдет страничный кадр в кэше
подкачки и вернет его физический адрес в запись Таблицы Страниц процес-
процесса В, что показано на рис. 17.8, г. И наоборот, если операция выгрузки закон-
закончится без параллельных операций загрузки, функция shrinkiist о удалит
страничный кадр из кэша подкачки и вернет страничный кадр buddy-системе,
как показано на рис. 17.8, д.
Рис. 17.8. Роль кэша подкачки
Вы можете считать кэш подкачки транзитной областью, содержащей деск-
дескрипторы анонимных страниц, которые выгружаются или загружаются в на-
настоящий момент. Когда операция выгрузки или загрузки завершается (в слу-
случае совместно используемой анонимной страницы выгрузка или загрузка
должна быть выполнена для всех процессов, работающих с этой страницей),
дескриптор анонимной страницы может быть удален из кэша подкачки11.
Реализация кэша подкачки
Кэш подкачки реализован с помощью структур и процедур кэша страниц,
которые описаны в разд. "Кэш страниц" главы 15. Вспомним, что основой
кэша страниц является набор базисных деревьев, позволяющий алгорит-
алгоритму быстро вычислить адрес дескриптора страницы по адресу объекта
addressspace, идентифицирующего владельца страницы, и по значению
смещения.
Страницы в кэше подкачки хранятся, как и страницы в кэше страниц, но
с соблюдением следующих специальных условий:
□ поле mapping дескриптора страницы содержит null;
□ флаг PGswapcache дескриптора страницы установлен;
□ поле private содержит идентификатор выгруженной страницы, ассоции-
ассоциированной с нею.
Кроме того, когда страница заносится в кэш подкачки, поле count дескрипто-
дескриптора страницы и счетчики страничного слота увеличиваются, потому что кэш
подкачки пользуется как страничным кадром, так и страничным слотом.
Наконец, для всех страниц в кэше подкачки используется одно адресное про-
пространство swapperspace, так что единственное базисное дерево, на которое
указывает поле swapperspace.pagetree, адресует страницы в кэше подкачки.
Поле nrpages адресного пространства swapperspace хранит количество стра-
страниц, содержащихся в кэше подкачки.
Вспомогательные функции кэша подкачки
Для работы с кэшем подкачки ядро пользуется несколькими функциями.
Они, главным образом, основаны на функциях, описанных в разд. "Кэш
страниц" в главе 15. Впоследствии мы покажем, как эти функции сравни-
11 В некоторых случаях кэш подкачки положительно влияет на производительность системы. Рас-
Рассмотрим серверный демон, обслуживающй запросы, создавая процессы-потомки. При значительной
нагрузке на систему может случиться, что страница процесса-родителя будет выгружена и больше
не будет загружена для него. Не будь кэша подкачки, каждому процессу-потомку пришлось бы за-
загружать страницу из области подкачки по требованию, реагируя на исключение "ошибка обращения
к странице".
тельно низкого уровня вызываются функциями высокого уровня для выгруз-
выгрузки и загрузки страниц, когда это необходимо. Основные функции для работы
с кэшем подкачки:
□ lookupswapcache () — находит страницу в кэше подкачки по идентифика-
идентификатору выгруженной страницы, переданному в качестве параметра, и воз-
возвращает адрес дескриптора страницы. Функция возвращает 0, если стра-
страница отсутствует в кэше. Чтобы найти требуемую страницу, функция вы-
вызывает функцию radixtreeiookupO, передавая ей в качестве параметров
указатель на swapperspace.pagetree (базисное дерево для страниц в кэше
подкачки) и идентификатор выгруженной страницы;
□ addtoswapcache () — заносит страницу в кэш подкачки. Эта функция
сначала вызывает функцию swapdupiicateo для проверки корректности
страничного слота, полученного в качестве параметра, и для увеличения
счетчика обращений страничного слота. Затем она вызывает функцию
radixtreeinsert (), чтобы занести страницу в кэш подкачки. В конце ра-
работы она увеличивает счетчик ссылок страницы и устанавливает флаги
PG_swapcache И PG_locked;
□ add_to_swap_cache () — аналогична функции add_to_swap_cache (), НО не
вызывает функцию swapdupiicateo перед занесением страницы в кэш
подкачки;
□ deiete_from_swap_cache() — удаляет страницу из кэша подкачки при по-
помощи функции radixtreedeiete (), уменьшает соответствующий счетчик
обращений в массиве swapmap и уменьшает счетчик ссылок страницы;
П f reepageandswapcache () — удаляет страницу из кэша подкачки, если
никакой процесс режима пользователя, кроме процесса current, не ссыла-
ссылается на соответствующий страничный слот. Затем уменьшает счетчик
ссылок страницы;
П free_pages_and_swap_cache () — аналогична функции free page and swap
cache (), но работает с набором страниц;
□ f reeswapandcache () — освобождает элемент подкачки и проверяет, на-
находится ли в кэше подкачки страница, на которую ссылается этот элемент.
Если никакой процесс режима пользователя, кроме процесса current, не
ссылается на эту страницу, или более 50% элементов подкачки заняты,
функция удаляет страницу из кэша подкачки.
Выгрузка страниц
В разд. "Утилизация при дефиците памяти" ранее в этой главе мы видели,
как алгоритм утилизации страничных кадров определяет, следует ли выгру-
жать данную анонимную страницу. В этом разделе мы покажем, как ядро вы-
выполняет выгрузку.
Занесение страничного кадра в кэш подкачки
Первым шагом операции выгрузки является подготовка кэша подкачки. Если
функция shrinkiist () определила, что страница является анонимной (функ-
(функция PageAnon () возвратила 1), а также что кэш подкачки не содержит соответ-
соответствующий страничный кадр (флаг PGswapcache дескриптора страницы сбро-
сброшен), ядро ВЫЗЫВает функцию add_to_swap ().
Функция addtoswap () выделяет новый страничный слот в области подкачки
и заносит страничный кадр (адрес его дескриптора передан в качестве пара-
параметра) в кэш подкачки. Функция выполняет следующие действия:
1. Вызывает функцию getswappage (), чтобы выделить новый страничный
слот (см. разд. "Выделение и освобождение страничного слота" ранее в
этой главе). В случае неудачи, например, если свободный страничный
слот не найден, возвращает 0.
2. Вызывает функцию addtopagecache (), передавая ей в качестве пара-
параметров индекс страничного слота, адрес дескриптора страницы и некото-
некоторые флаги выделения памяти.
3. Устанавливает флаги PGuptodate и PGdirty дескриптора страницы, чтобы
заставить функцию shrinkiist () записать страницу на диск.
4. Возвращает 1 (успех).
Обновление записей Таблицы Страниц
После завершения функции addtoswap () функция shrinkiisto вызывает
функцию trytounmapo, которая определяет адрес каждой записи таблицы
страниц режима пользователя, ссылающейся на анонимную страницу, и за-
заносит в эту запись идентификатор выгруженной страницы. Все это описано в
разд. "Обратное отображение для анонимных страниц"ранее в этой главе.
Запись страницы в область подкачки
Следующее действие, которое надо выполнить для выгрузки, заключается в
записи содержимого страницы в область подкачки. Эта операция пересылки
данных запускается функцией shrinkiisto, которая проверяет, установлен
ли флаг PGdirty страничного кадра, а затем вызывает функцию pageouto
(см. рис. 17.5).
Как было сказано в разд. "Утилизация при дефиците памяти" ранее в этой
главе, функция pageout() устанавливает ПОЛЯ дескриптора writeback_control
и вызывает метод writepage объекта addressspace данной страницы. Метод
writepage объекта swapper_state реализован функцией swap_writepage ().
Функция swapwritepage(), со своей стороны, выполняет следующие дей-
действия:
1. Убеждается, что хотя бы один процесс режима пользователя ссылается на
страницу. Если это не так, функция удаляет страницу из кэша подкачки и
возвращает 0. Эта проверка необходима, потому что какой-нибудь процесс
может конкурировать с алгоритмом утилизации страничных кадров и ос-
освободить страницу после проверки, выполненной функцией shrinkiist ().
2. Вызывает функцию getswapbio (), чтобы выделить и инициализировать
дескриптор Ыо (см. разд. "Структура Ыо" главы 14). Функция вычисляет
адрес дескриптора области подкачки по идентификатору выгружаемой
страницы. Затем она обходит списки интервалов подкачки, чтобы опреде-
определить начальный сектор страничного слота на диске. Дескриптор bio будет
содержать запрос на одну страницу данных (страничный слот), а в качест-
качестве метода завершения указана функция endswapbiowrite ().
3. Устанавливает флаг PGwriteback дескриптора страницы и теги обратной
записи в базисном дереве кэша подкачки (см. разд. "Теги базисного дере-
дерева" главы 15). Кроме того, сбрасывает флаг PGiocked.
4. Вызывает функцию submitbio (), передавая ей команду write и адрес де-
дескриптора bio.
5. Возвращает 0.
По окончании операции ввода/вывода выполняется функция end_swap_
bio_write(). Она возобновляет выполнение всех процессов, ожидающих
сброса флага PGwriteback, сбрасывает флаг PGwriteback и соответствующие
теги в базисном дереве и освобождает дескриптор bio, использованный в
операции ввода/вывода.
Удаление страничного кадра из кэша подкачки
Последний шаг операции выгрузки выполняется все той же функцией
shrinkiist о. Если она убеждается, что никакой процесс не пытался обра-
обратиться к страничному кадру во время ввода/вывода данных, она вызывает
ФУНКЦИЮ deletef romswapcache (), чтобы удалить СТраниЧНЫЙ кадр ИЗ КЭШа
подкачки. Поскольку кэш подкачки был единственным владельцем страницы,
страничный кадр возвращается buddy-системе.
Загрузка выгруженных страниц
Загрузка имеет место, когда процесс пытается обратиться к странице, которая
была выгружена на диск. Обработчик исключения "ошибка обращения к
странице" запускает операцию загрузки при следующих условиях:
□ страница, включающая адрес, вызвавший исключение, является коррект-
корректной, т. е. принадлежит области памяти текущего процесса;
□ страница отсутствует в памяти, т. е. флаг Present в записи Таблицы Стра-
Страниц сброшен;
□ запись Таблицы Страниц, ассоциированная с этой страницей, содержит
ненулевое значение, но бит Dirty сброшен. Это означает, что запись со-
содержит идентификатор выгруженной страницы (см. главу 9).
Если все перечисленные условия удовлетворены, функция handie_pte_
fault () вызывает вполне подходящую функцию doswappage (), чтобы загру-
зить необходимую выгруженную страницу.
Функция do_swap_page()
Функция doswappage () принимает следующие параметры:
□ mm— адрес дескриптора памяти, принадлежащего процессу, вызвавшему
исключение "ошибка обращения к странице";
□ vma — адрес дескриптора области памяти, которая включает в себя адрес
address;
□ address — линейный адрес, из-за обращения к которому возникло исклю-
исключение;
□ pagetabie — адрес записи Таблицы Страниц, которая отображает адрес
address;
□ pmd— адрес среднего каталога страниц, который отображает адрес
address;
□ origpte— содержимое записи Таблицы Страниц, которая отображает
адрес address;
□ writeaccess — флаг, показывающий, было ли обращение к странице по-
попыткой прочитать или записать ее.
В отличие от других функций, doswappage () никогда не возвращает 0. Она
возвращает 1, если страница уже находится в кэше подкачки (незначительная
ошибка); 2, если страница была прочитана из области подкачки (серьезная
ошибка); и -1,если во время загрузки выгруженной страницы возникла
ошибка.
Функция выполняет следующие действия:
1. Извлекает идентификатор выгруженной страницы из origpte.
2. Вызывает функцию pteunmap()? чтобы освободить любое имеющееся
временное отображение ядра для Таблицы Страниц, созданное функцией
handieramf auit () (см. главу 9). Как было сказано в главе 8, отображение
страницы в адресное пространство ядра требуется, когда нужно обратить-
обратиться к таблице страниц, находящейся в верхней памяти.
3. Освобождает спин-блокировку pagetabieiock дескриптора памяти (он
был получен функцией handieptefauit(), вызвавшей описываемую
функцию).
4. Вызывает функцию lookupswapcache (), чтобы проверить, содержит ли
кэш подкачки страницу, определяемую идентификатором выгруженной
страницы. Если страница уже находится в кэше, функция переходит к ша-
шагу 6.
5. Вызывает функцию swapin_readahead(), Чтобы Прочитать ИЗ области ПОД-
качки группу максимум из 2п страниц, включающую запрошенную стра-
страницу. Значение п хранится в переменной pagecluster и обычно равно
трем12. Каждая страница читается функцией readswapcacheasync ().
6. Еще раз вызывает функцию readswapcacheasync(), чтобы загрузить
именно ту страницу, к которой обратился процесс, вызвавший исключение
"ошибка обращения к странице". Этот шаг может показаться излишним,
но в действительности таковым не является. Функция swapinreadahead ()
могла и не прочитать запрошенную страницу, например, потому что пере-
переменная pagecluster равна 0, или потому что функция пыталась прочитать
группу страниц, в которую входил свободный или дефектный страничный
СЛОТ (SWAPMAPBAD). С другой СТОрОНЫ, если функции swapinreadahead ()
удалось прочитать страницу, функция readswapcacheasync () быстро за-
завершит работу, потому что сразу обнаружит страницу в кэше подкачки.
7. Если, несмотря на все усилия, запрошенная страница не была добавлена
в кэш подкачки, значит, скорее всего, другой управляющий тракт ядра
уже загрузил эту страницу для одного из клонов данного процесса. Та-
Такая возможность проверяется временным захватом спин-блокировки
pagetabieiock и сравнением записи, на которую указывает параметр
pagetabie с содержимым параметра origpte. Если они отличаются, зна-
значит, страница уже загружена каким-то другим управляющим трактом
12 Системный администратор может изменить это значение, отредактировав файл /proc/sys/vm/page-
cluster. Опережающее чтение при загрузке выгруженных страниц можно отключить, записав ноль в
переменную page_cluster.
ядра, и функция возвращает 1 (незначительная ошибка); в противном
случае она возвращает -1 (неудача).
8. На этом шаге мы знаем, что страница находится в кэше подкачки. Если
она была фактически загружена (серьезная ошибка обращения), функция
вызывает функцию grabswaptoken (), чтобы попытаться захватить жетон
защиты от выгрузки (см. разд. "Жетон защиты от выгрузки" ранее в
этой главе).
9. Вызывает функцию mark_page_accessed() (см. разд. "Списки давно неис-
неиспользуемых страниц (LRU)"ранее в этой главе) и блокирует страницу.
10. Получает СПИН-блокировку page_table_lock.
11. Проверяет, не загрузил ли другой управляющий тракт ядра эту страницу
для одного из клонов данного процесса. Если это так, освобождает спин-
блокировку pagetablelock, разблокирует страницу и возвращает 1 (не-
(незначительная ошибка обращения).
12. Вызывает функцию swap_free(), чтобы уменьшить счетчик обращений
страничного слота, соответствующего идентификатору выгруженной
страницы.
13. Убеждается, что кэш подкачки заполнен хотя бы на 50% (значение поля
nrswappages меньше ПОЛОВИНЫ значения total_swap_pages). Если ЭТО
действительно так, проверяет, принадлежит ли страница только процессу,
вызвавшему ошибку обращения (или одному из его клонов). В случае по-
положительного результата проверки удаляет страницу из кэша подкачки.
14. Увеличивает поле rss дескриптора памяти, принадлежащего процессу.
15. Обновляет запись Таблицы Страниц, чтобы процесс мог найти страницу.
Функция выполняет это, занося физический адрес запрошенной страницы
и биты защиты, хранящиеся в поле vmpageprot области памяти, в запись
Таблицы Страниц, адрес которой находится в параметре pagetabie. Кро-
Кроме того, если попытка обратиться к странице, вызвавшая исключение,
была попыткой записи, а процесс, совершивший ее, является единствен-
единственным владельцем страницы, то функция также устанавливает флаги Dirty
и Read/write, чтобы предотвратить бесполезное исключение "копирова-
"копирование при записи".
16. Снимает блокировку со страницы.
17. Вызывает функцию page_add_anon_rmap (), чтобы вставить анонимную
страницу в структуры объектно-базированного обратного отображе-
отображения (см. разд. "Обратное отображение для анонимных страниц" ранее
в этой главе).
18. Если параметр writeaccess равен 1, функция вызывает функцию do_wp_
page (), чтобы сделать копию страничного кадра (см. разд. "Копирование
при записи " главы 9).
19. Освобождает спин-блокировку nim->page_tabie_iock и возвращает код ret:
1 (незначительная ошибка обращения) или 2 (серьезная ошибка).
Функция read__swap_cache__async()
ФуНКЦИЯ readswapcacheasync () ВЫЗЫВаеТСЯ, КОГДа ядро ДОЛЖНО Загрузить
выгруженную страницу. Она принимает три параметра:
□ entry — идентификатор выгруженной страницы;
□ vma — указатель на область памяти, которая должна содержать страницу;
□ addr — линейный адрес страницы.
Как мы знаем, перед обращением к области подкачки функция должна про-
проверить, находится ли требуемый страничный кадр в кэше подкачки. Поэтому
она выполняет следующие действия:
1. Вызывает функцию radixtreeiookupo, чтобы найти в базисном дереве
объекта swapperspace страничный кадр в позиции, заданной идентифика-
идентификатором выгруженной страницы entry. Если страница найдена, функция
увеличивает ее счетчик ссылок и возвращает адрес ее дескриптора.
2. Страница отсутствует в кэше подкачки. Тогда функция вызывает функцию
aiiocpageso, чтобы выделить новый страничный кадр. Если свободных
страничных кадров нет, функция возвращает 0 (свидетельствуя о дефици-
дефиците памяти в системе).
3. Вызывает функцию addtoswapcache (), чтобы занести дескриптор ново-
нового страничного кадра в кэш подкачки. Как было сказано ранее в
разд. "Вспомогательные функции кэша подкачки", эта функция тоже
блокирует страницу.
4. Предыдущий шаг мог закончиться неудачно, если функция add_to_swap_
cache о нашла дубликат страницы в кэше подкачки. Например, процесс
мог быть блокирован на шаге 2, что позволило другому процессу запус-
запустить операцию загрузки для того же страничного слота. В таком случае
функция освобождает страничный кадр, выделенный на шаге 2, и возоб-
возобновляет работу с шага 1.
5. Вызывает функцию lrucacheaddactive (), чтобы занести страницу в ак-
активный список LRU (см. разд. "Списки давно неиспользуемых страниц
(LRU) "ранее в этой главе).
6. Дескриптор нового страничного кадра находится в кэше подкачки. Функ-
Функция вызывает функцию swapreadpage(), чтобы прочитать содержимое
страницы из области подкачки. Вызванная функция аналогична функции
swapwritepage (), описанной ъ разд. "Выгрузка страниц" ранее в этой гла-
главе. Она сбрасывает флаг PGuptodate дескриптора страницы, вызывает
функцию getswapbio (), чтобы выделить и проинициализировать деск-
дескриптор Ыо для операции ввода/вывода, и вызывает функцию submitbio (),
чтобы передать запрос на ввод/вывод слою работы с блочными устройст-
устройствами.
7. Возвращает адрес дескриптора страницы.
ГЛАВА 18
Файловые системы Ext2 и Ext3
В этой главе мы завершаем наше пространное обсуждение ввода/вывода и
файловых систем. Мы рассмотрим тонкости, которые ядро должно прини-
принимать во внимание при взаимодействии с конкретной файловой системой. По-
Поскольку Вторая расширенная файловая система Ext2 является "родной" для
Linux и используется практически в любой Linux-системе, мы, естественно,
выберем ее для обсуждения. Кроме всего прочего, Ext2 иллюстрирует мно-
множество удачных подходов в реализации функциональности современных вы-
высокопроизводительных файловых систем. Читатель может не сомневаться,
что и другие файловые системы, поддерживаемые операционной системой
Linux, обладают многими интересными функциональными возможностями,
но у нас нет места для рассмотрения их всех.
После обзора файловой системы Ext2 в разд. "Общие характеристики фай-
файловой системы Ext2" мы опишем необходимые ей структуры данных, как де-
делали это в предыдущих главах. Поскольку мы рассматриваем конкретный
способ хранения данных на диске, нам придется описать две версии одних и
тех же структур. В разд. "Структуры Ext2 на диске" показаны структуры,
которые Ext2 хранит на диске, а в разд. "Структуры Ext2 в памяти"— соот-
соответствующие версии, хранящиеся в памяти.
Затем мы перейдем к операциям, выполняемым файловой системой.
В разд. "Создание файловой системы Ext2" мы обсудим, как Ext2 создается в
разделе диска. Далее описаны действия, выполняемые ядром при обращении
к диску. Это, по большей части, низкоуровневые операции, связанные с вы-
выделением места на диске для индексных дескрипторов и блоков данных.
В последнем разделе этой главы мы дадим краткий обзор файловой системы
Ext3, которая является следующим шагом в развитии файловой системы Ext2.
Общие характеристики
файловой системы Ext2
В Unix-подобных операционных системах применяются файловые системы
нескольких типов. Хотя файлы во всех этих файловых системах имеют общее
подмножество атрибутов, требуемых некоторыми API-вызовами POSIX, та-
такими как stat (), каждая файловая система реализована по-своему.
В основе первых версий Linux лежала файловая система MINIX. По мере раз-
развития Linux появилась Ext FS (Extended Filesystem, Расширенная файловая
система). Она включала в себя ряд важных расширений, но имела неудовле-
неудовлетворительную производительность. В 1994 г. была представлена Ext2 (Second
Extended Filesystem, Вторая расширенная файловая система). Она имела не-
несколько новых функциональных возможностей и оказалась такой производи-
производительной и устойчивой, что в настоящее время вместе со своим отпрыском
Ext3 является самой широко используемой файловой системой в Linux.
Эффективность Ext2 обусловлена следующими ее свойствами:
□ при создании файловой системы Ext2 системный администратор может
выбрать оптимальный размер блока (от 1024 до 4096 байтов), в зависимо-
зависимости от ожидаемой средней длины файла. Например, блок в 1024 байта
предпочтителен, когда средняя длина файла меньше нескольких тысяч
байтов, потому что в этом случае внутренняя фрагментация будет меньше,
т. е. будет меньше несоответствие между длиной файла и той частью дис-
диска, которую он занимает {см. главу S, где обсуждается внутренняя фраг-
фрагментация динамической памяти). Зато большие размеры блока обычно
предпочтительнее длиной более, чем несколько тысяч байтов, потому что
в таком случае будет меньше пересылок данных на диск и обратно, что
понизит накладные расходы системы;
□ при создании файловой системы Ext2 системный администратор может
выбрать количество индексных дескрипторов, разрешенных для раздела
данного размера, в зависимости от ожидаемого количества файлов, хра-
хранящихся в этом разделе. Тем самым увеличивается эффективно исполь-
используемое пространство на диске;
□ файловая система объединяет блоки на диске в группы. Каждая группа
включает в себя блоки данных и индексные дескрипторы, расположенные
на смежных дорожках. Благодаря такой структуре, к файлам, хранящимся
в одной группе, можно обратиться, потратив на поиск, в среднем, меньше
времени;
□ файловая система заранее выделяет блоки данных под обычные файлы до
того, как они будут фактически использованы. Таким образом, когда файл
увеличивается в размере, несколько физически смежных блоков уже заре-
зарезервированы, что уменьшает фрагментацию файла;
□ поддерживаются быстрые символьные ссылки (см. главу!). Если сим-
символьная ссылка представляет собой короткий путь к файлу (максимум
60 символов), ее можно хранить в индексном дескрипторе и, следователь-
следовательно, транслировать, не читая блок данных.
Кроме того, Ext2 обладает и другими свойствами, делающими ее устойчивой
и гибкой:
□ аккуратная реализация обновления файла, минимизирующая последствия
сбоев системы. Например, при создании новой жесткой ссылки на файл
сначала увеличивается счетчик жестких ссылок в индексном дескрипторе
на диске, а затем в соответствующий каталог добавляется новое имя. Та-
Таким образом, если аппаратный сбой произойдет после обновления индекс-
индексного дескриптора, но до изменения каталога, каталог останется коррект-
корректным, хотя счетчик жестких ссылок индексного дескриптора будет показы-
показывать неверное значение. Удаление файла не приводит к катастрофическим
результатам, хотя его блоки и нельзя будет автоматически утилизировать.
Если бы действия совершались в обратном порядке (изменение каталога
до обновления индексного дескриптора), то тот же аппаратный сбой стал
бы причиной опасного несоответствия: удаление оригинальной жесткой
ссылки повлекло бы за собой удаление блоков данных с диска, а новый
элемент каталога продолжал бы ссылаться на уже несуществующий ин-
индексный дескриптор. Если номером этого индексного дескриптора впо-
впоследствии воспользуется другой файл, запись в некорректный элемент ка-
каталога исказит содержимое этого файла;
П поддержка автоматической проверки непротиворечивости файловой сис-
системы на этапе загрузки. Проверка выполняется внешней программой
e2fsck, которая может быть запущена не только после сбоя системы, но и
после определенного количества операций монтирования файловой сис-
системы (счетчик увеличивается после каждого монтирования) или по исте-
истечении установленного времени с момента последней проверки;
□ поддержка неизменяемых файлов (таких, которые нельзя модифициро-
модифицировать, удалять и переименовывать) и файлов, доступных только для допол-
дополнения (таких, что данные могут быть только записаны в их конец);
□ совместимость с семантиками идентификатора группы пользователя ново-
нового файла, принятыми как в Unix System V Release 4, так и в BSD. Семан-
Семантика SVR4 предполагает, что у нового файла тот же идентификатор груп-
группы, что и у создавшего его процесса; в BSD же новый файл наследует
идентификатор группы от каталога, в котором находится. Ext2 имеет оп-
опцию монтирования, определяющую, какой семантикой пользоваться.
Хотя Ext2 является зрелой и стабильной файловой системой, в настоящее
время рассматривается вопрос о включении в нее дополнительных функцио-
функциональных возможностей1. Некоторые уже закодированы и доступны в виде
внешних патчей. Другие еще только проектируются, но в индексном деск-
дескрипторе Ext2 для них уже зарезервированы поля. Самыми важными из новых
функциональных возможностей являются следующие:
□ фрагментация блоков— системные администраторы обычно выбирают
большой размер для блоков на диске, потому что приложения часто рабо-
работают с большими файлами. В результате для маленьких файлов неэконом-
неэкономно расходуется пространство на диске. Эту проблему можно решить,
обеспечив хранение нескольких файлов в различных фрагментах одного
блока;
П прозрачная работа со сжатыми и зашифрованными файлами — эти новые
опции, задаваемые при создании файла, позволяют пользователям про-
прозрачно хранить сжатые и/или зашифрованные версии своих файлов на
диске;
□ логическое удаление — опция undelete позволяет пользователям без труда
восстановить удаленный файл, если в том возникнет необходимость;
□ журналирование — позволяет избежать длительной проверки, которая ав-
автоматически выполняется при внезапном размонтировании файловой сис-
системы (например, в результате краха системы).
На практике ни одна из этих функциональных возможностей не была офици-
официально введена в файловую систему Ext2. Можно сказать, что Ext2 стала
жертвой собственного успеха. Вплоть до последнего времени она была самой
предпочитаемой файловой системой, принятой большинством компаний-
дистрибьютеров Linux, и миллионы пользователей, ежедневно работающих с
ней, с подозрением воспринимают любые попытки заменить Ext2 какой-
нибудь другой файловой системой.
Самой притягательной возможностью, отсутствующей у Ext2, является жур-
журналирование, необходимое в сильно загруженных серверах. Чтобы обеспе-
обеспечить плавный переход, было решено не вводить журналирование в Ext2. Вме-
Вместо этого была создана новая журналируемая, полностью совместимая с Ext2,
и мы обсудим ее в разд. "Файловая система Ext3" далее в этой главе. Поль-
Пользователи, не нуждающиеся в журналировании, могут продолжать работать со
старой доброй Ext2, а остальные, вероятно, перейдут на новую файловую
1 Фактически, на момент подготовки русской редакции данной книги, была начата разработка фай-
файловой системы Ext4, являющейся дальнейшим развитием Ext2/3. Возможно, к тому моменту, когда
вы будете читать эти строки, она уже будет включена в ядро. — Прим. науч. ред.
систему. Сегодня во многих дистрибутивах Ext3 принята в качестве стан-
стандартной файловой системой.
Структуры Ext2 на диске
Файловая система Ext2 никогда не обращается к первому блоку каждого сво-
своего раздела, потому что этот блок зарезервирован для загрузочного сектора
(см. приложение 1). Оставшаяся часть раздела Ext2 разбита на группы бло-
блоков, каждая из которых имеет схему компоновки, изображенную на рис. 18.1.
Как видно из рисунка, некоторые структуры данных должны занимать только
один блок, в то время как для других может потребоваться несколько блоков.
Все группы блоков в файловой системе имеют одинаковый размер и распо-
расположены последовательно, так что ядро может вычислить местонахождение
группы блоков на диске просто по ее целочисленному индексу.
Рис. 18.1. Компоновка раздела и группы блоков в Ext2
Группы блоков уменьшают фрагментацию файлов, поскольку ядро стремится
хранить блоки данных файла в одной группе блоков файловой системы, если
это возможно. Каждый блок в группе содержит один из следующих элемен-
элементов информации:
□ копию суперблока файловой системы;
□ копию дескрипторов групп блоков
□ битовую карту блоков данных;
□ битовую карту индексных дескрипторов;
□ таблицу индексных дескрипторов;
□ данные файлов, т. е. блоки данных.
Если блок не содержит осмысленную информацию, говорят, что он свободен.
Как видно из рис. 18.1, суперблок и дескрипторы групп дублируются в каж-
каждой группе блоков. Только суперблок и дескрипторы групп, включенные в
группу блоков 0, используются ядром, а остальные суперблоки и дескрипто-
дескрипторы остаются неизмененными. Более того, ядро их даже не просматривает.
Когда программа e2fsck проверяет непротиворечивость состояния файловой
системы, она читает суперблок и дескрипторы групп, хранящиеся в группе
блоков 0, и копирует их во все другие группы блоков. Если произойдет порча
данных, и главный суперблок или главные дескрипторы групп в группе бло-
блоков 0 станут некорректными, системный администратор может проинструк-
проинструктировать программу e2fsck, чтобы она читала старые копии суперблока и де-
дескрипторов групп, хранящиеся в других группах блоков. Обычно эти избы-
избыточные копии содержат достаточно информации, чтобы позволить e2fsck
привести раздел Ext2 в корректное состояние.
Сколько существует групп блоков? Это зависит как от размера раздела, так и
от размера блока. Основное ограничение заключается в том, что битовая кар-
карта блоков, применяемая для идентификации свободных и занятых блоков
внутри группы, должна храниться в одном блоке. Следовательно, в каждой
группе блоков может быть максимум 8x6 блоков, где Ъ — размер блока в
байтах. Тогда общее количество групп блоков примерно равно s/($xb), где
s — размер раздела в блоках.
В качестве примера рассмотрим 32-гигабайтный раздел Ext2 с размером бло-
блока 4 Кбайт. В этом случае каждая битовая карта в 4 Кбайт описывает
32 Кбайт блоков данных, т.е. 128 Мбайт. Следовательно, необходимо
256 групп блоков. Ясно, что, чем меньше размер блока, тем больше групп
блоков.
Суперблок
В файловой системе Ext2 суперблок диска хранится в структуре ext2_super_
block, поля которой перечислены в табл. 18.12. Типы u8, ui6 и u32 обо-
обозначают целые без знака длиной 8, 16 и 32 бита соответственно, а типы s8,
si6 и s32 — целые со знаком соответствующей длины. Чтобы явно задать
порядок хранения байтов слова или двойного слова на диске, ядро пользуется
типами 1е1б, 1е32, Ье1б и Ье32. Первые два типа обозначают прямой
порядок байтов (младшие байты имеют большие адреса), в то время как по-
последние два типа обозначают обратный порядок байтов (старшие байты
имеют большие адреса).
Поле sinodescount хранит количество индексных дескрипторов, а поле
sbiockscount — количество блоков в файловой системе Ext2.
2 Для обеспечения совместимости файловых систем Ext2 и Ext3 структура ext2_super_block
имеет несколько полей, специфичных для Ext3. В табл. 18.1 они не приводятся.
Таблица 18.1. Поля суперблока Ext2
Тип Поле Описание
1е32 s_inodes_count Общее количество индексных дескрипто-
дескрипторов
1е32 sblockscount Размер файловой системы в блоках
1е32 s_r_blocks_count Количество зарезервированных блоков
1е32 s_f ree_blocks_count Счетчик свободных блоков
1е32 s_f ree_inodes_count Счетчик свободных индексных
дескрипторов
1е32 s_first_data_block Номер первого полезного блока (всегда 1)
1е32 s_log_block_size Размер блока
1е32 s_log_frag_size Размер фрагмента
1е32 s_blocks_per_group Количество блоков в группе
1е32 s_f rags_per_group Количество фрагментов в группе
1е32 s_inodes_per_group Количество индексных дескрипторов
1е32 s_mtime Время последней операции монтирования
1е32 s_wtime Время последней операции записи
lei б s_mnt_count Счетчик операций монтирования
lei б s_max_mnt_count Количество операций монтирования,
после которого выполняется проверка
lei б sjnagic Магическая сигнатура
lei б sstate Флаг состояния
lei б s_errors Поведение при обнаружении ошибок
lei б s_minor_rev_level Младший уровень ревизии
1е32 s_lastcheck Время последней операции проверки
1е32 s_checkinterval Время между проверками
1е32 s_creator_os ОС, в которой была создана файловая
система
1е32 srevlevel Уровень ревизии файловой системы
lei6 s_def_resuid UID по умолчанию для зарезервирован-
зарезервированных блоков
lei б s_def_resgid Идентификатор группы по умолчанию для
зарезервированных блоков
1е32 s_f irstino Номер первого не зарезервированного
индексного дескриптора
Таблица 18.1 (окончание)
Тип Поле Описание
1е1б s_inode_size Размер структуры индексного дескрипто-
дескриптора на диске
lei б s_block_group_nr Номер группы блоков для этого
суперблока
1е32 s_feature_compat Битовая карта совместимых
функциональных возможностей
1е32 s_feature_incompat Битовая карта несовместимых
функциональных возможностей
1е32 s_f eaturerocompat Битовая карта совместимых функцио-
функциональных возможностей только для чтения
и8 [16] suuid 128-битовый идентификатор файловой
системы
char [16] s_volume_name Имя тома
char [64] s_last_mounted Путь к последней точке монтирования
1е32 s_algorithm_usage_bitmap Используется при сжатии
u8 spreallocblocks Количество предварительно выделяемых
блоков
u8 s_prealloc_dir_blocks Количество блоков, выделяемых предва-
предварительно для каталогов
ul6 s_paddingl Выравнивание по границе слова
и32 [204] s_reserved Нули для заполнения 1024 байтов
Поле siogbiocksize выражает размер блока в виде степени двойки, ис-
используя 1024 байта в качестве единицы измерения. Так, 0 обозначает
1024-байтовые блоки, 1 — блоки по 2048 байт и т. д. В настоящее время поле
siogfragsize равно полю siogbiocksize, потому что фрагментация еще
не реализована.
Поля s_blocks_per_group, s_frags_per_group И s_inodes_per_group Хранят
соответственно количество блоков, фрагментов и индексных дескрипторов
в группе блоков.
Некоторые блоки на диске зарезервированы для суперпользователя (или ка-
какого-нибудь другого пользователя или группы пользователей, заданных в по-
полях sdefresuid и sdefresgid). Эти блоки позволяют системному админи-
администратору продолжать пользоваться файловой системой, даже когда для обыч-
обычных пользователей не останется свободных блоков.
Поля s_mnt_count, s_max_mnt_count, s_lastcheck И s_checkinterval обеспечи-
вают автоматическую проверку файловой системы на этапе загрузки. Они
заставляют программу e2fsck выполняться после того, как будет выполнено
заданное количество монтирований файловой системы, или по истечении за-
заданного интервала времени после последней проверки. (Оба типа проверок
можно задать одновременно.) Проверка непротиворечивости файловой сис-
системы выполняется также на этапе загрузки, если файловая система не была
корректно размонтирована (например, из-за краха системы) или если ядро
обнаружило в ней ошибки. Поле sstate содержит ноль, если файловая сис-
система смонтирована или не была корректно размонтирована, единицу, если
она была размонтирована корректно, и двойку, если файловая система со-
содержит ошибки.
Дескриптор группы и битовая карта
Каждая группа блоков имеет собственный дескриптор, структуру ext2_
groupdesc, поля которой перечислены в табл. 18.2.
Таблица 18.2. Поля дескриптора группы Ext2
Тип Поле Описание
1е32 bg_block_bitmap Номер блока с битовой картой блоков
1е32 bg_inode_bitmap Номер блока с битовой картой индексных
дескрипторов
1е32 bg_inode_table Номер первого блока таблицы индексных
дескрипторов
lei 6 bgfreeblockscount Количество свободных блоков в группе
lei б bg_free_inodes_count Количество свободных индексных дескрипторов
в группе
lei б bg_used_dirs_count Количество каталогов в группе
lei6 bg_pad Выравнивание по границе слова
1е32 [3] bg_reserved Нули для заполнения 24 байтов
Поля bg_free_blocks_count, bg_free_inodes_count И bg_used_dirs_count ИС-
пользуются при выделении новых индексных дескрипторов и блоков данных.
Эти поля определяют самый подходящий блок, в котором следует выделить
ту или иную структуру. Битовые карты являются последовательностями би-
битов, в которых 0 означает, что соответствующий индексный дескриптор или
блок данных свободен, а 1 — что он занят. Поскольку каждая битовая карта
должна храниться в одном блоке, а блок может иметь размер 1024, 2048
или 4096 байтов, одна битовая карта описывает состояние 8192, 16 384 или
32 768 блоков.
Таблица индексных дескрипторов
Таблица индексных дескрипторов состоит из ряда блоков, каждый из кото-
которых содержит заранее определенное количество индексных дескрипторов.
Номер первого блока таблицы индексных дескрипторов хранится в поле
bginodetable дескриптора группы.
Все индексные дескрипторы имеют одинаковый размер, 128 байтов. Блок
длиной 1024 байта содержит 8 индексных дескрипторов, а блок из
4096 байтов — 32 индексных дескриптора. Чтобы узнать, сколько блоков за-
занято таблицей индексных дескрипторов, разделите количество индексных
дескрипторов в группе (которое хранится в поле sinodespergroup супер-
суперблока) на количество индексных дескрипторов в блоке.
Каждый индексный дескриптор в файловой системе Ext2 представляет собой
структуру ext2_inode, поля которой перечислены в табл. 18.3.
Таблица 18.3. Поля индексного дескриптора Ext2
Тип Поле Описание
lei б i_mode Тип файла и права доступа
lei б i_uid Идентификатор владельца
1е32 i_size Длина файла в байтах
1е32 iatime Время последнего обращения к файлу
1е32 i_ctime Время последнего изменения индексного
дескриптора
1е32 i_mtime Время последнего изменения содержимого
файла
1е32 i_dtime Время удаления файла
lei б i_gid Идентификатор группы пользователя
1е1б i_links_count Счетчик жестких ссылок
1е32 i_blocks Количество блоков данных в файле
1е32 i_f lags Флаги файла
union osdl Данные, специфичные для операционной
системы
1е32 [ext2_n_blocks] i_block Указатели на блоки данных
1е32 i_generation Версия файла (используется, когда к фай-
файлу обращается сетевая файловая система)
Таблица 18.3 (окончание)
Тип Поле Описание
1е32 i_f ile_acl Список управления доступом к файлу
1е32 idiracl Список управления доступом к каталогу
1е32 i_f addr Адрес фрагмента
union osd2 Данные, специфичные для операционной
системы
Многие поля, имеющие отношение к спецификациям POSIX, аналогичны по-
полям объекта "индексный дескриптор" в виртуальной файловой системе и
фактически были обсуждены в главе 12. Остальные поля специфичны для
реализации Ext2 и, в основном, имеют отношение к выделению блоков.
В частности, поле isize содержит фактическую длину файла в байтах, а по-
поле ibiocks — количество блоков данных (измеряемое в единицах по
512 байтов), выделенных файлу.
Значения в полях isize и ibiocks не обязательно связаны. Поскольку файл
всегда занимает целое число блоков, непустой файл получает хотя бы один
блок данных (поскольку фрагментация блоков еще не реализована), и значе-
значение в поле isize может оказаться меньше, чем 512xi_biocks. С другой сто-
стороны, как мы увидим в разд. "Дыры в файлах" далее в этой главе, файл мо-
может содержать "дыры". В этом случае isize может превышать 512xi_biocks.
Поле ibiock является массивом из ext2_n_blocks (обычно 15) указателей на
блоки и используется для идентификации блоков данных, выделенных файлу
(см. разд. "Адресация блоков данных" далее в этой главе).
32 бита, зарезервированные под поле isize, ограничивают размер файла че-
четырьмя гигабайтами. Фактически старший бит поля isize не используется,
так что максимальный размер файла равен 2 Гбайт. Однако файловая система
Ext2 использует один "грязный трюк", позволяющий иметь файлы большего
размера в системах с 64-разрядным процессором, таким как Opteron фирмы
AMD или IBM PowerPC G5. Суть в том, что поле idiracl индексного деск-
дескриптора, которое не используется для обычных файлов, представляет
32-битовое расширение поля isize. Поэтому размер файла хранится в ин-
индексном дескрипторе в виде 64-битового целого. Шестидесятичетырехбито-
Шестидесятичетырехбитовая версия файловой системы Ext2 в определенной степени совместима
с 32-битовой версией, поскольку Ext2, созданная в 64-битовой архитектуре,
может быть смонтирована в 32-битовой архитектуре и наоборот. В 32-
битовой архитектуре нельзя обратиться к большому файлу иначе, чем открыв
его с установленным флагом olargefile (cm. разд. "Системный вызов
ореп()" главы 12).
Вспомним, что модель VFS требует, чтобы каждый файл имел собственный
номер индексного дескриптора. В Ext2 нет необходимости хранить на диске
отображение номера индексного дескриптора в соответствующий номер бло-
блока, поскольку последний может быть вычислен по номеру группы блоков и
относительной позиции внутри таблицы индексных дескрипторов. Предпо-
Предположим, например, что каждая группа блоков содержит 4096 индексных деск-
дескрипторов, и мы хотим узнать адрес индексного дескриптора номер 13 021.
В этом случае индексный дескриптор принадлежит третьей группе блоков, и
его адрес на диске хранится в 733-й записи таблицы индексных дескрипто-
дескрипторов. Как видите, номер индексного дескриптора является ключом, позво-
позволяющим служебным процедурам файловой системы Ext2 быстро читать его
с диска.
Расширенные атрибуты индексного дескриптора
Формат индексного дескриптора в Ext2 является чем-то вроде смирительной
рубашки для разработчиков файловых систем. Его длина должна быть сте-
степенью двойки, чтобы не допустить внутренней фрагментации блоков, в кото-
которых хранится таблица индексных дескрипторов. На самом деле, большинство
из 128 байтов индексного дескриптора Ext2 настолько "забиты" информаци-
информацией, что для новых полей осталось очень мало места. С другой стороны, уве-
увеличение длины индексного дескриптора до 256 байтов было бы слишком рас-
расточительно, не говоря о проблемах совместимости с файловыми системами
Ext2, использующими другую длину индексного дескриптора.
Для преодоления этого неудобства были введены расширенные атрибуты.
Они хранятся на диске в блоке, выделенном за пределами любого индексного
дескриптора. Поле ifiieaci индексного дескриптора указывает на блок,
содержащий расширенные атрибуты. Разные индексные дескрипторы,
имеющие один набор расширенных атрибутов, могут совместно пользоваться
одним блоком.
У каждого расширенного атрибута есть имя и значение. Они кодируются в
виде символьных массивов переменной длины в соответствии с содержимым
дескриптора ext2_xattr_entry. На рис. 18.2 показана схема расположения
расширенных атрибутов внутри блока в Ext2. Каждый атрибут разбит на две
части: дескриптор ext2_xattr_entry вместе с именем атрибута помещен в на-
начало блока, а значение атрибута — в конец. Элементы в начале блока упоря-
упорядочены по именам атрибутов, а позиции значений фиксированы, поскольку
определяются порядком выделения атрибутов.
Существует много системных вызовов для установки, получения, перечис-
перечисления и удаления расширенных атрибутов файла. Системные вызовы
setxattrO, lsetxattrO и fsetxattrO устанавливают расширенный атрибут.
Они различаются способом обработки символьных ссылок и способом указа-
указания файла (с помощью пути или дескриптора). Системные вызовы
getxattrо, lgetxattro и fgetxattro возвращают значение расширенного
атрибута, а вызовы listxattr (), liistxattr () и f listxattr () перечисляют все
расширенные атрибуты файла. Наконец, системные вызовы removexattr (),
lremovexattr о и f removexattr о удаляют расширенные атрибуты файла.
Рис. 18.2. Компоновка блока, содержащего расширенные атрибуты
Списки управления доступом
Списки управления доступом были предложены довольно давно для усовер-
усовершенствования механизма защиты файлов в файловых системах Unix. Вместо
классификации пользователей файла по трем категориям (владелец, группа и
все остальные) с каждым файлом можно ассоциировать список управления
доступом. Благодаря такому списку пользователь может для каждого своего
файла указать имена конкретных пользователей (или групп пользователей) и
предоставленные им привилегии.
Linux 2.6 полностью поддерживает списки управления доступом, для чего
пользуется расширенными атрибутами индексного дескриптора. Фактически
расширенные атрибуты были введены, в основном, для поддержки этих спи-
списков. Таким образом, библиотечные функции chad о, setfacio и getfacio,
позволяющие манипулировать списком управления доступом к файлу, осно-
основываются на системных вызовах setxattro и getxattr о, представленных
в предыдущем разделе.
К сожалению, материалы рабочей группы, которая определила расширения
безопасности в рамках семейства стандартов POSIX 1003.1, так и не были
оформлены в виде нового стандарта POSIX. В результате списки управления
доступом поддерживаются сегодня файловыми системами разнообразных
типов во многих Unix-подобных операционных системах, но эти реализации
имеют ряд тонких различий.
Как файлы разных типов
используют блоки на диске
Файлы разных типов, поддерживаемых в Ext2 (обычные файлы, каналы
и т. д.), используют блоки данных по-разному. Некоторые файлы не хранят
данные на диске, и, следовательно, им вообще не нужны блоки данных.
В этом разделе обсуждаются требования для каждого файла из табл. 18.4.
Таблица 18.4. Типы файлов Ext2
Тип Описание
0 Неизвестен
1 Обычный файл
2 Каталог
3 Символьное устройство
4 Блочное устройство
5 Именованный канал
6 Сокет
7 Символьная ссылка
Обычный файл
Обычные файлы — самый распространенный тип, и в этой главе им уделяет-
уделяется больше всего внимания. Обычному файлу блоки данных требуются только
тогда, когда он приступает собственно к хранению данных. Когда обычный
файл создается, он пуст, и блоки данных ему не нужны; кроме того, он может
стать пустым в результате работы системных вызовов truncate о или open о.
Обе ситуации являются вполне типичными. Например, когда вы выдаете
команду оболочки, включающую в себя строку >fiiename, оболочка создает
пустой файл или усекает существующий.
Каталог
В файловой системе Ext2 каталоги реализованы как специальный тип файлов,
блоки которых содержат имена файлов и номера соответствующих индекс-
индексных дескрипторов. В частности, такие блоки содержат структуры типа
ext2_dir_entry_2. Поля такой структуры приведены в табл. 18.5. Структура
имеет переменную длину, поскольку ее последнее поле name является масси-
массивом переменной длины, который может содержать до ext2_name_len символов
(обычно 255). Кроме того, по соображениям эффективности длина элемента
каталога всегда кратна 4, и поэтому в конец имени файла добавляется необ-
необходимое количество нулевых символов (\о). Поле nameien содержит факти-
фактическую длину файла (рис. 18.3).
Таблица 18.5. Поля элемента каталога Ext2
Тип Поле Описание
1е32 inode Номер индексного дескриптора
lei б reclen Длина элемента каталога
u8 name_len Длина имени файла
u8 f ile_type Тип файла
char [EXT2_NAME_LEN] name Имя файла
Поле filetype содержит тип файла (см. табл. 18.4). Поле reclen можно ин-
интерпретировать как указатель на следующий корректный элемент каталога:
это смещение, которое следует добавить к начальному адресу элемента ката-
каталога, чтобы получить начальный адрес следующего элемента. Чтобы удалить
элемент каталога, достаточно записать 0 в его поле inode и соответственно
увеличить значение поля reclen у предыдущего элемента. Взгляните внима-
внимательно на столбец reclen на рис. 18.3, и вы увидите, что элемент oldfile был
удален, поскольку поле reclen элемента usr равно 12+16 (длины элементов
usr и oldfile).
Рис. 18.3. Пример каталога Ext2
Символьная ссылка
Как было сказано выше, если путь в символьной ссылке содержит до
60 символов, он хранится в поле ibiock индексного дескриптора, которое
является массивом из 15 целых чисел по 4 байта. Следовательно, в этом слу-
случае отдельный блок данных не требуется. Если путь состоит из более чем
60 символов, нужен один блок данных.
Файл устройства, канал и сокет
Для файлов этих типов блоки данных не требуются. Вся необходимая ин-
информация хранится в индексном дескрипторе.
Структуры Ext2 в памяти
Для эффективности большая часть информации, хранящейся в структурах
раздела Ext2 на диске, копируется в оперативную память во время монтиро-
монтирования файловой системы. Это позволяет ядру избежать впоследствии многих
операций чтения с диска. Чтобы получить представление о том, как часто из-
изменяются структуры данных, рассмотрим некоторые фундаментальные опе-
операции:
□ когда создается новый файл, значения в поле s_f reeinodescount супер-
суперблока Ext2 и в поле bgfreeinodescount соответствующего дескриптора
группы должны быть уменьшены;
□ если ядро добавляет данные в конец существующего файла так, что коли-
количество блоков данных, выделенных файлу, увеличивается, то значения в
поле s_f ree_blocks_count суперблока Ext2 И В поле bg_f ree_blocks_count
соответствующего дескриптора группы должны быть изменены;
□ даже простая перезапись порции существующего файла требует обновле-
обновления поля swtime суперблока Ext2.
Поскольку все дисковые структуры в Ext2 хранятся в блоках раздела Ext2,
ядро пользуется кэшем страниц для поддержания их в актуальном состоянии
(см. разд. "Запись грязных страниц на диск" главы 15).
В табл. 18.6 для каждого типа данных, имеющего отношение к файловым
системам и файлам Ext2, указаны структуры, представляющие эти данные на
диске, структуры, которые ядро хранит в памяти, и информация о том, на-
насколько интенсивно применяется кэширование. Данные, обновляемые очень
часто, кэшируются всегда, т. е. эти данные постоянно присутствуют в памяти
и включены в кэш страниц, пока соответствующий раздел Ext2 не будет раз-
размонтирован. Ядро добивается этого тем, что все время поддерживает счетчик
обращений к странице больше 0.
Данные, которые "никогда не кэшируются", не записываются в кэш, потому
что не представляют осмысленной информации. И наоборот, "всегда кэши-
руемые" данные постоянно присутствуют в оперативной памяти, так что нет
необходимости читать их с диска (впрочем, периодически их следует записы-
записывать на диск). Между этими крайностями лежит динамический режим кэши-
кэширования. В этом режиме данные хранятся в памяти столько же времени,
сколько используется ассоциированный объект (индексный дескриптор, блок
данных или битовая карта). Когда файл закрывается, или блок данных удаля-
удаляется, алгоритм утилизации страничных кадров может удалить соответствую-
соответствующие данные из кэша.
Таблица 18.6. VFS-образы структур Ext2
Тип Структура на диске "фуктура Режим кэширования
в пэмяти
Суперблок ext2_super_block ext2_sb_info Всегда котируется
Дескриптор группы ext2_group_desc ext2_group_desc Всегда кэшируется
Битовая карта блоков Массив битов Массив битов Динамический
в блоке в буфере
Битовая карта Массив битов Массив битов Динамический
индексных в блоке в буфере
дескрипторов
Индексный ext2_inode ext2_inode_info Динамический
дескриптор
Блок данных Массив байтов Буфер VFS Динамический
Свободный индекс- ext2_inode Нет Никогда не кэшируется
ный дескриптор
Свободный блок Массив байтов Нет Никогда не кэшируется
Интересно отметить, что битовые карты индексных дескрипторов и блоков не
хранятся в памяти постоянно, а читаются с диска по мере необходимости. На
самом деле, многих операций чтения с диска удается избежать благодаря кэ-
кэшу страниц, который хранит в памяти недавно использованные блоки (см.
разд. "Хранение блоков в кэше страниц" в главе 15/.
Объект-суперблок Ext2
Как было сказано в разд. "Суперблоки" в главе 12, поле sfsinfo суперблоки
виртуальной файловой системы указывает на структуру, содержащую дан-
данные, специфичные для файловой системы.
3 В Linux 2.4 и более ранних версиях недавно использованные битовые карты индексных дескрип-
дескрипторов и блоков хранились в специальных кэшах фиксированного размера.
В случае Ext2 это поле указывает на структуру типа ext2_sb_info, которая
содержит следующую информацию:
□ большинство полей суперблока на диске;
□ указатель ssbh на голову буфера, содержащего суперблок;
□ указатель ses на буфер, содержащий суперблок;
□ количество дескрипторов групп, которое может быть упаковано в один
блок (поле s_desc_per_block);
□ указатель sgroupdesc на массив голов буферов, содержащих дескрипто-
дескрипторы групп (обычно одного элемента достаточно);
□ другие данные, относящиеся к состоянию монтирования, опциям монти-
монтирования и т. д.
На рис. 18.4 изображены связи между структурами ext2_sb_info и буферами
и их головами, относящимися к суперблоку Ext2 и дескрипторам групп.
Когда ядро монтирует файловую систему Ext2, оно вызывает функцию
ext2_fiii_super о для выделения места под структуры и заполнения их дан-
данными, прочитанными с диска (см. разд. "Монтирование типичной файловой
системы" главы 12). Приведем упрощенное описание этой функции, делая
упор на выделение памяти под буферы и дескрипторы.
Выделяет дескриптор ext2_sb_info и сохраняет его адрес в поле s_f sinfo
объекта-суперблока, переданного в качестве параметра.
Рис. 18.4. Структура ext2_sb_info
Вызывает функцию bread (), чтобы выделить буфер в странице буферов и
соответствующую голову буфера, а также чтобы прочитать суперблок с диска
в буфер. Как было сказано в разд. "Поиск блоков в кэше страниц" в главе 75,
выделение не выполняется, если блок уже хранится в странице буферов в кэ-
кэше страниц и еще не устарел. Затем функция сохраняет адрес головы буфера
в поле ssbh суперблока Ext2.
Выделяет массив байтов (по одному на каждую группу) и сохраняет его адрес
в поле sdebts дескриптора ext2_sb_info (см. раздел "Создание индексных
дескрипторов" далее в этой главе).
Выделяет массив указателей на головы буферов, по одному на каждый деск-
дескриптор группы, и сохраняет адрес массива в поле sgroupdesc дескриптора
ext2_sb_info.
Повторно вызывает функцию bread (), чтобы выделить буферы и прочитать
с диска блоки, содержащие дескрипторы групп Ext2, и сохраняет адреса го-
голов буферов в массиве sgroupdesc, выделенном на предыдущем шаге.
Выделяет индексный дескриптор и элемент каталога для корневого каталога
и устанавливает некоторые поля объекта-суперблока так, чтобы было воз-
возможно прочитать корневой индексный дескриптор с диска.
Очевидно, что все структуры, выделенные функцией ext2_fiii_super(), ос-
остаются в памяти после того, как она возвратит управление; они будут осво-
освобождены только после размонтирования файловой системы Ext2. Когда ядро
должно модифицировать поле суперблока Ext2, оно просто записывает нуж-
нужное значение в нужное место соответствующего буфера и помечает буфер как
"грязный".
Индексный дескриптор Ext2
При открытии файла выполняется анализ пути к нему. Для каждого элемента
пути, отсутствующего в кэше элементов каталога, создаются два новых объ-
объекта — элемент каталога и индексный дескриптор (см. разд. "Стандартный
анализ пути" главы 12). Когда виртуальная файловая система обращается к
индексному дескриптору Ext2 на диске, она создает дескриптор индексного
дескриптора, имеющий тип ext2_inode_infо. Этот дескриптор содержит сле-
следующую информацию:
□ индексный дескриптор виртуальной файловой системы (см. табл. 12.3),
целиком сохраненный в поле vf sinode;
□ большинство полей индексного дескриптора на диске, которые отсутст-
отсутствуют у индексного дескриптора виртуальной файловой системы;
П индекс группы блоков ibiockgroup, которой принадлежит индексный
дескриптор (см. разд. "Структуры Ext2 на диске"ранее в этой главе);
□ ПОЛЯ inextallocblock И i_next_alloc_goal, содержащие соответственно
логический и физический номера последнего выделенного файлу блока на
диске;
□ ПОЛЯ ipreallocblock И iprealloccount, ИСПОЛЬЗуемые ДЛЯ Предвари-
тельного выделения блоков (см. разд. "Выделение блока данных" далее в
этой главе);
□ поле xattrsem, являющееся семафором чтения/записи, позволяющим чи-
читать расширенные атрибуты одновременно с данными файла;
□ поля iaci и idef auitaci, указывающие на списки управления доступом
к файлу.
Для работы с файлами Ext2 метод суперблока aiiocinode реализован функ-
функцией ext2_alloc_inode(). Она вначале получает дескриптор ext2_inode_info
из кэша slab-аллокатора ext2_inode_cachep, а затем возвращает адрес индекс-
индексного дескриптора, встроенного в дескриптор ext2_inode_infо.
Создание файловой системы Ext2
Создание файловой системы Ext2 на диске обычно состоит из двух этапов.
Первый заключается в форматировании диска, чтобы драйвер мог читать и
записывать блоки. Современные жесткие диски поставляются отформатиро-
отформатированными, и их переформатировать не нужно. Гибкие диски могут быть от-
отформатированы в Linux с помощью утилиты, например, superformat или
fdformat. Второй этап включает в себя создание файловой системы, т. е. уста-
установку значений структур, подробно описанных в предыдущих разделах этой
главы.
Файловые системы Ext2 создаются утилитой mke2fs. Перечислим ее настрой-
настройки по умолчанию, которые могут быть модифицированы пользователем при
помощи параметров командной строки:
□ размер блока: 1024 байта (значение по умолчанию для небольших файло-
файловых систем);
□ размер фрагмента: равен размеру блока (фрагментация блоков еще не реа-
реализована);
□ количество выделенных индексных дескрипторов: один на каждые
8192 байта;
□ процент зарезервированных блоков: 5%.
Утилита выполняет следующие действия:
1. Инициализирует суперблок и дескрипторы групп.
2. Необязательный шаг: проверяет, содержит ли раздел дефектные блоки.
Если содержит, утилита создает их список.
3. Для каждой группы блоков резервирует столько блоков, сколько необхо-
необходимо для хранения суперблока, дескрипторов групп, таблицы индексных
дескрипторов и двух битовых карт.
4. Инициализирует нулями битовую карту индексных дескрипторов и бито-
битовую карту блоков данных каждой группы блоков.
5. Инициализирует таблицу индексных дескрипторов у каждой группы бло-
блоков.
6. Создает каталог/root.
7. Создает каталог lost+found, используемый программой e2fsck для хране-
хранения потерянных и найденных дефектных блоков.
8. Обновляет битовую карту индексных дескрипторов и битовую карту бло-
блоков данных в группе, в которой были только что созданы два каталога.
9. Собирает дефектные блоки (если таковые найдены) в каталоге lost+found.
Рассмотрим, как файловая система Ext2 инициализируется на дискете
1,44 Мбайт утилитой mke2fs с настройками по умолчанию.
Будучи смонтированной, Ext2 представляется виртуальной файловой системе
в виде тома, состоящего из 1412 блоков, каждый по 1024 байта. Чтобы прове-
проверить содержимое диска, мы можем воспользоваться командой Unix:
$ dd if=/dev/fdO bs=lk count=1440 I od -txl -Ax > /tmp/dump_hex
Мы получим в каталоге /tmp файл с шестнадцатеричным дампом содержимо-
содержимого диска4.
Взглянув на этот файл, мы увидим, что, ввиду ограниченной емкости диска,
одного дескриптора группы оказалось достаточно. Мы также заметим, что
зарезервировано 72 блока E% от 1440). Поскольку в соответствии с настрой-
настройками по умолчанию таблица индексных дескрипторов должна иметь один
индексный дескриптор на каждые 8192 байта, мы убеждаемся, что в
23 блоках хранится 184 индексных дескриптора.
В табл. 18.7 суммированы результаты создания файловой системы Ext2 на
гибком диске с настройками по умолчанию.
Таблица 18.7. Распределение блоков Ext2 на гибком диске
Блок Содержимое
0 Загрузочный блок
1 Суперблок
4 Большая часть информации по файловой системе Ext2 может быть также получена с помощью
утилит dumpe2fs и debugfs.
Таблица 18.7 (окончание)
Блок Содержимое
2 Блок, содержащий единственный дескриптор группы
3 Битовая карта блоков данных
4 Битовая карта индексных дескрипторов
5-27 Таблица индексных дескрипторов. Индексные дескрипторы вплоть
до десятого: зарезервированы (второй — корневой); индексный
дескриптор 11: lost+found; индексные дескрипторы с 12 по 184: свободны
28 Корневой каталог (включает в себя каталоги ".",".." и lost+found
29 Каталог lost+found (включает в себя каталоги "."и "..")
30-40 Зарезервированные блоки, предварительно выделенные каталогу lost+found
41-1439 Свободные блоки
Методы Ext2
Многие методы виртуальной файловой системы, описанные в главе 12, име-
имеют реализацию в Ext2. Поскольку описание их всех заняло бы целую книгу,
мы ограничимся лишь кратким обзором. Разобравшись со структурами дан-
данных на диске и в памяти, читатель легко разберется и в коде функций, реали-
реализующих эти методы в Ext2.
Операции суперблока Ext2
Многие операции суперблока виртуальной файловой системы реализованы и
В Ext2. Вот ИХ перечень: alloc_inode, destroy_inode, read_inode, write_inode,
delete_inode, put_super, write_super, statfs, remount_f s И clear_inode. Адреса
методов суперблока хранятся в массиве указателей ext2_sops.
Операции индексного дескриптора Ext2
Некоторые операции индексного дескриптора виртуальной файловой систе-
системы имеют в Ext2 специфическую реализацию, зависящую от типа файла, на
который ссылается индексный дескриптор.
Операции индексного дескриптора для обычных файлов и каталогов файло-
файловой системы Ext2 приведены в табл. 18.8, а назначение каждого метода опи-
описано в главе 12. В таблице отсутствуют методы, которые не определены (со-
(соответствующий указатель равен null) как для обычных файлов, так и для ка-
каталогов. Вспомним, что, если метод не определен, виртуальная файловая
система либо вызывает функцию общего назначения, либо вообще ничего не
предпринимает. Адреса методов Ext2 для обычных файлов и каталогов хра-
хранятся В таблицах ext2_file_inode_operations И ext2_dir_inode_operations
соответственно.
Таблица 18.8. Операции индексного дескриптора Ext2
для обычных файлов и каталогов
JSSSSrar" |°°ь.ч»ыйфайл I—,
create NULL ext2_create()
lookup NULL ext2_lookup()
link NULL ext2_link()
unlink NULL ext2_unlink()
symlink NULL ext2_symlink ()
mkdir NULL ext2_mkdir ()
rmdir NULL ext2_rmdir ()
mknod NULL ext2_mknod ()
rename NULL ext2_rename ()
truncate ext2_truncate() NULL
permission ext2_permission() ext2_permission()
setattr ext2_setattr() ext2_setattr()
setxattr generic_setxattr() generic_setxattr()
getxattr generic_getxattr() generic_getxattr()
listxattr ext2_listxattr() ext2_listxattr()
removexattr generic_removexattr() generic_removexattr()
Операции индексного дескриптора Ext2 для символьных ссылок приведены в
табл. 18.9 (неопределенные методы опущены). Существует две группы сим-
символьных ссылок: быстрые символьные ссылки, представляющие пути, кото-
которые могут быть целиком помещены в индексные дескрипторы, и обычные
символьные ссылки, представляющие более длинные пути. Поэтому сущест-
существует два набора операций индексного дескриптора, хранящиеся в таблицах
ext2_fast_symlink_inode_operations И ext2_symlink_inode_operations COOT-
ветственно.
Если индексный дескриптор ссылается на файл символьного устройства,
файл блочного устройства или именованный канал (см. разд. "FIFO-фаты " в
главе 19), то операции индексного дескриптора не зависят от файловой сие-
темы. Они задаются В таблицах chrdev_inode_operations, blkdev_inode_
operations И f ifo_inode_operations соответственно.
Таблица 18.9. Операции индексного дескриптора
для быстрых и обычных символьных ссылок
Операция индексного Быстрая символьная Обычная символьная
дескриптора VFS ссылка ссылка
create NULL ext2_create()
lookup NULL ext2_lookup()
link NULL ext2_link()
unlink NULL ext2_unlink()
symlink NULL ext2_symlink ()
mkdir NULL ext2_mkdir ()
rmdi r NULL ex12_rmdi r ()
mknod NULL ext2_mknod()
rename NULL ext2_rename()
truncate ext2_truncate() NULL
permission ext2_permission() ext2_permission()
setattr ext2_setattr() ext2_setattr()
setxattr generic_setxattr() generic_setxattr()
getxattr generic_getxattr() generic_getxattr()
listxattr ext2_listxattr() ext2_listxattr()
removexattr generic_removexattr() generic_removexattr()
Файловые операции Ext2
Файловые операции, специфичные для Ext2, перечислены в табл. 18.10. Как
видно из таблицы, несколько методов виртуальной файловой системы реали-
реализованы функциями общего назначения, существующими во многих файловых
системах. Адреса этих методов хранятся в таблице ext2_f ileoperations.
Таблица 18.10. Файловые операции Ext2
Файловая операция VFS Метод Ext2
llseek generic_file_llseek()
read generic_file_read()
write generic_file_write()
Таблица 18.10 (окончание)
Файловая операция VFS Метод Ext2
aio_read generic_file_aio_read()
aio_write generic_file_aio_write()
ioctl ext2_ioctl()
ramap generic_f ile_rranap ()
open generic_file_open()
release ext2_release_file()
f sync ext2_sync_f ile ()
readv generic_file_readv()
writev generic_file_writev()
sendfile generic_file_sendfile()
Обратите внимание, что в Ext2 методы read и write реализованы, соответст-
соответственно, функциями generic_file_read() И genericfilewrite (), которые ОПИ-
саны ъразд. "Чтение из файла" и "Запись в файл" в главе 16.
Управление пространством на диске в Ext2
Расположение файла на диске отличается от того, как файл представлен про-
программисту. Принципиальных отличий два: блоки могут быть разбросаны по
всему диску (хотя файловая система и делает все возможное, чтобы размес-
разместить файл в последовательных блоках, сократив тем самым время поиска), и
файлы могут казаться больше, чем на самом деле, потому что программа мо-
может вносить в них "дыры" (с помощью системного вызова lseek ()).
В этом разделе мы опишем, как файловая система Ext2 управляет дисковым
пространством — как она выделяет и освобождает индексные дескрипторы и
блоки. При этом ей приходится решать две основные проблемы:
□ управление пространством диска должно происходить так, чтобы отсутст-
отсутствовала фрагментация файлов, т. е. физическое разбиение файлов на не-
несколько небольших фрагментов, находящихся в несмежных блоках. Фраг-
Фрагментация увеличивает среднее время последовательных операций чтения
файлов, поскольку магнитные головки должны часто менять положение
по ходу операции . Эта проблема аналогична проблеме внешней фрагмен-
фрагментации оперативной памяти (см. главу 8);
5 Заметим, что фрагментация файлов по нескольким группам блоков (отрицательное явление)
сильно отличается от пока не реализованной фрагментации блоков (положительное явление), кото-
которая позволит хранить несколько файлов в одном блоке.
□ управление дисковым пространством должно быть эффективно с точки
зрения времени. Иными словами, ядро должно быть в состоянии быстро
вычислить по смещению в файле номер логического блока в разделе Ext2.
При этом должно быть минимизировано количество обращений к табли-
таблицам адресации, хранящимся на диске, поскольку каждое промежуточное
обращение значительно увеличивает среднее время операции.
Создание индексных дескрипторов
Функция ext2 newinode () создает новый индексный дескриптор Ext2 и воз-
возвращает адрес соответствующего объекта (или null, если операция закончи-
закончилась неудачей). Функция тщательно выбирает группу блоков для нового ин-
индексного дескриптора. Это делается для того, чтобы каталоги, не связанные
друг с другом, были расположены в разных группах, и, в то же время, файлы
находились в тех же группах, что их родительские каталоги. Чтобы сбалан-
сбалансировать количество обычных файлов и каталогов в одной группе блоков,
в Ext2 было введено понятие "долга" для каждой группы блоков.
Функция ext2_new_inode () принимает два параметра: dir— адрес объекта
"индексный дескриптор", ссылающегося на каталог, в который должен быть
помещен новый индексный дескриптор, и mode — индикатор типа создавае-
создаваемого индексного дескриптора. Второй параметр также содержит флаг монти-
монтирования mssynchronous (см. разд. "Монтирование типичной файловой сис-
системы" главы 12), который требует приостановки текущего процесса, пока не
будет создан индексный дескриптор. Функция выполняет следующие дей-
действия:
1. Вызывает функцию newinode () для выделения нового объекта, индексно-
индексного дескриптора виртуальной файловой системы, инициализирует его поле
isb адресом суперблока, хранящимся в поле dir->i_sb, а затем заносит
новый объект в список используемых индексных дескрипторов и в список
индексных дескрипторов, принадлежащих суперблоку (см. главу 12).
2. Если новый индексный дескриптор соответствует каталогу, функция вы-
вызывает функцию findgrouporlov(), чтобы найти подходящую группу
блоков для этого каталога6. Вызванная функция реализует следующую эв-
эвристику:
• каталоги, родителем которых является корневой каталог файловой сис-
системы, должны быть распределены по всем группам блоков. Таким об-
6 Файловая система Ext2 может быть также смонтирована с флагом, заставляющим ядро при-
прибегнуть к более простой старой стратегии выделения, реализованной с помощью функции
find_group_dir().
разом, функция ищет группу, у которой количество свободных индекс-
индексных дескрипторов и количество свободных блоков выше среднего. Ес-
Если такой группы не окажется, функция переходит на два шага вперед;
• вложенные каталоги (родитель которых — не корневой каталог) долж-
должны быть размещены в группе родителя, если она удовлетворяет сле-
следующим требованиям:
п группа не содержит слишком много каталогов;
п группа имеет достаточное количество свободных индексных деск-
дескрипторов;
п группа имеет небольшой "долг" (величина "долга" хранится в мас-
массиве счетчиков, на который указывает поле sdebts дескриптора
ext2_sb_inf о; долг увеличивается каждый раз, когда добавляется но-
новый каталог, и уменьшается при добавлении файла другого типа);
( Примечание )
Если группа родителя не удовлетворяет этим требованиям, функция выбирает
первую группу, которая им удовлетворяет. Если такой группы нет, функция пе-
переходит к следующему шагу.
• если подходящую группу найти не удалось, применяется следующее
"правило для отступления". Функция начинает поиск с группы блоков,
содержащей родительский каталог, и останавливает свой выбор на пер-
первой группе, в которой количество свободных индексных дескрипторов
превышает среднее по всем группам.
3. Если новый индексный дескриптор не соответствует каталогу, функция
вызывает функцию f indgroupother () для размещения его в группе бло-
блоков, имеющей свободный индексный дескриптор. Вызванная функция вы-
выбирает группу, начиная просмотр с той, которая содержит родительский
каталог, и двигаясь дальше. Более конкретно, она делает следующее:
• выполняет быстрый логарифмический поиск, начиная с группы блоков,
содержащей родительский каталог dir. Алгоритм просматривает \og(n)
групп, где п — их общее количество. Алгоритм работает, пока не най-
найдет подходящую группу блоков. Например, если / — номер группы, в
которой начался поиск, то алгоритм просматривает группы с номерами
/ mod(«), /+1 mod(», /+1+2 mod(w), z+1+2+4 mod(«) и т. д.;
• если логарифмический поиск не позволил найти группу со свободным
индексным дескриптором, функция выполняет исчерпывающий линей-
линейный поиск, начиная с группы блоков, содержащей родительский ката-
каталог dir.
4. Вызывает фуНКЦИЮ read_inode_bitmap () , чтобы ПОЛуЧИТЬ битовую Карту
индексных дескрипторов выбранной группы блоков, и ищет в карте пер-
первый нулевой бит, получая номер первого свободного индексного деск-
дескриптора на диске.
5. Выделяет индексный дескриптор на диске: устанавливает соответствую-
соответствующий бит в карте индексных дескрипторов и помечает буфер, который ее
содержит, как "грязный". Кроме того, если файловая система была смон-
смонтирована с флагом mssynchronous (см. разд. "Монтирование типичной
файловой системы" главы 12), функция вызывает функцию sync_dirty_
buffer(), чтобы запустить операцию ввода/вывода, и ждет окончания пе-
пересылки данных.
6. Уменьшает поле bgf reeinodescount дескриптора группы. Если новый
индексный дескриптор соответствует каталогу, функция увеличивает по-
поле bguseddirscount и помечает буфер, содержащий дескриптор груп-
группы, как "грязный".
7. Увеличивает или уменьшает счетчик группы в массиве sdebts супербло-
суперблока, в зависимости от того, ссылается индексный дескриптор на обычный
файл или каталог.
8. Уменьшает поле s_freeinodes_counter структуры ext2_sb_info. Кроме ТО-
ТОГО, если новый индексный дескриптор соответствует каталогу, функция
увеличивает поле sdirscounter в структуре ext2_sb_inf о.
9. Устанавливает в единицу поле sdirt суперблока и помечает буфер, со-
содержащий его, как "грязный".
10. Устанавливает в единицу поле sdirt суперблока и виртуальной файло-
файловой системы.
11. Инициализирует поля индексного дескриптора. В частности, устанавли-
устанавливает номер индексного дескриптора i по и копирует значение
xtime.tv_sec В ПОЛе i_atime, i_mtime И i_ctime. Кроме ТОГО, Загружает В
поле ibiockgroup структуры ext2_inode_info индекс группы блоков.
Описание этих полей дано в табл. 18.3.
12. Инициализирует списки управления доступом, принадлежащего индекс-
индексному дескриптору.
13. Заносит новый объект "индексный дескриптор" в хеш-таблицу inode_
hashtable И ВЫЗЫВаеТ фуНКЦИЮ markinodedirty (), чтобы перенести ИН-
дексный дескриптор в список "грязных" индексных дескрипторов супер-
суперблока (см. главу 12).
14. Вызывает функцию ext2_preread_inode(), чтобы прочитать с диска блок,
содержащий индексный дескриптор, и поместить этот блок в кэш стра-
ниц. Этот вид опережающего чтения применяется, поскольку велика ве-
вероятность, что недавно созданный индексный дескриптор будет вскоре
записан на диск.
15. Возвращает адрес нового объекта "индексный дескриптор".
Удаление индексных дескрипторов
Функция ext2_f reeinode () удаляет индексный дескриптор, представленный
объектом "индексный дескриптор", адрес которого, inode, она получает в ка-
качестве параметра. Ядро должно вызвать эту функцию после ряда заключи-
заключительных операций, затрагивающих внутренние структуры и данные в самом
файле. Функция вызывается после удаления объекта "индексный дескриптор"
из хеш-таблицы, после удаления последней жесткой ссылки на этот индекс-
индексный дескриптор из каталога и после усечения файла до нулевой длины
с целью утилизации всех его блоков (см. разд. "Освобождение блока данных"
далее в этой главе). Функция выполняет следующие действия:
1. Вызывает функцию ciearinode (), которая выполняет следующее:
• удаляет все "грязные" косвенные буферы, ассоциированные с этим ин-
индексным дескриптором (см. разд. "Адресация блоков данных" далее в
этой главе). Они собраны в списке, голова которого находится в поле
private_list объекта address_space ПО адресу inode->i_data (см. гла-
вУ15);
• если флаг ilock индексного дескриптора установлен, значит, некото-
некоторые буферы индексного дескриптора вовлечены в операцию вво-
ввода/вывода. В таком случае функция приостанавливает текущий про-
процесс, пока не завершится эта операция;
• вызывает метод ciearinode объекта-суперблока, если он определен.
Сама файловая система Ext2 этот метод не определяет;
• если индексный дескриптор ссылается на файл устройства, функция
удаляет объект "индексный дескриптор" из списка индексных дескрип-
дескрипторов, принадлежащего этому устройству. Этот список имеет корень
либо в поле list дескриптора символьного устройства cdev (см.
разд. "Драйверы символьных устройств" главы 13), либо в поле
bdinodes дескриптора блОЧНОГО устройства block_device (CM.
разд. "Блочные устройства" главы 14);
• устанавливает состояние индексного дескриптора в значение iclear
(содержимое этого объекта больше не имеет смысла).
2. Вычисляет индекс группы блоков, содержащей индексный дескриптор на
диске, по номеру индексного дескриптора и количеству индексных деск-
дескрипторов в каждой группе блоков.
3. Вызывает функцию read_inode_bitmap(), чтобы получить битовую карту
индексных дескрипторов.
4. Увеличивает значение в поле bgfreeinodescount дескриптора группы.
Если удаляемый индексный дескриптор соответствует каталогу, функция
уменьшает значение поля bguseddirscount. Затем функция помещает
буфер, содержащий дескриптор группы, как "грязный".
5. Если удаляемый индексный дескриптор соответствует каталогу, функция
уменьшает значение поля sdirscounter структуры ext2_sb_info, уста-
устанавливает флаг sdirt суперблока в единицу и помечает буфер, содержа-
содержащий суперблок, как "грязный".
6. Сбрасывает бит, соответствующий индексному дескриптору на диске,
в битовой карте индексных дескрипторов и помечает буфер, содержащий
ее, как "грязный". Кроме того, если файловая система была смонтирована
С флаГОМ MS_SYNCHRONIZE, фуНКЦИЯ ВЫЗЫВает фунКЦИЮ sync_dirty_buffer ()
и ждет завершения операции записи буфера, содержащего битовую карту.
Адресация блоков данных
Каждый не пустой обычный файл состоит из некоторого количества блоков
с данными. Ссылаться на такие блоки можно либо по их относительной по-
позиции внутри файла (по номеру блока в файле), либо по их позиции в разделе
диска (по логическому номеру блока).
Вычисление логического номера блока данных по смещению/внутри файла
состоит из двух шагов:
1. Вычисление номера блока в файле по смещению/ Это индекс блока, со-
содержащего символ, имеющий смещение/
2. Преобразование номера блока в файле в логический номер блока.
Поскольку файлы в Unix не содержат управляющих символов, вычислить
номер блока, содержащего/й символ файла, очень просто: найдите частное
от деления/на размер блока в файловой системе и округлите его до ближай-
ближайшего целого снизу.
Предположим, например, что размер блока равен 4 Кбайт. Если / мень-
меньше 4096, символ содержится в первом блоке данных файла, т. е. блоке но-
номер 0. Если/больше или равно 4096 и меньше 8192, символ находится во
втором блоке с номером 1 и т. д.
С номерами блоков в файле все ясно. Однако преобразование номера блока
в файле в логический номер блока выполняется не столь прямолинейно, по-
поскольку блоки в Ext2 не обязательно являются соседними на диске.
Следовательно, файловая система должна предоставить способ хранения со-
соответствия между номером каждого блока в файле и логическим номером
этого блока на диске. Это отображение, имеющееся уже в ранних версиях
Unix от AT&T, реализовано частично внутри индексного дескриптора. Оно
также применяет специализированные блоки, в которых содержатся допол-
дополнительные указатели, являющиеся расширением индексного дескриптора,
используемым при работе с большими файлами.
Поле ibiock индексного дескриптора на диске является массивом из
ext2_n_blocks элементов, содержащим логические номера блоков. Далее мы
будем предполагать, что константа ext2_n_blocks имеет значение по умолча-
умолчанию, а именно 15. Массив является первой частью большой структуры, кото-
которая изображена на рис. 18.5. Как видно из рисунка, пятнадцать элементов
массива имеют четыре различных типа:
П первые 12 элементов содержат логические номера, соответствующие пер-
первым 12 блокам файла, т. е. блокам с 0 по 11;
□ элемент с индексом 12 содержит логический номер блока, называемого
косвенным. Этот блок представляет массив второго эшелона, содержащий
логические номера блоков. Они соответствуют номерам блоков в файле
от 12 до 6/4+11, где Ъ — размер блока в файловой системе (каждый логи-
логический номер блока занимает 4 байта, отсюда деление на 4 в приведенной
формуле). Следовательно, ядро должно искать в этом элементе указатель
на блок, а в том блоке — еще один указатель на блок, который и содержит
данные файла;
Рис. 18.5. Структуры, применяемые для адресации блоков файла
□ элемент с индексом 13 содержит логический номер косвенного блока,
в котором находится массив второго эшелона с логическими номерами
блоков. В свою очередь, элементы этого массива являются указателями
на массивы третьего эшелона, содержащие логические номера бло-
блоков, соответствующие номерам блоков в файле в диапазоне от 6/4+12
доF/4J+F/4)+11;
□ наконец, элемент с индексом 14, использует тройное косвенное указание.
Массивы четвертого эшелона содержат логические номера, соответ-
соответствующие номерам блоков в файле в диапазоне от F/4J+F/4)+12
доF/4K+F/4J+F/4)+11.
На рис. 18.5 число внутри блока обозначает номер блока в файле. Стрелки,
обозначающие логические номера блоков, хранящиеся в элементах массива,
показывают, как ядро проходит по косвенным блокам, чтобы достичь блока,
в котором содержатся данные файла.
Обратите внимание, насколько этот механизм благоприятен для небольших
файлов. Если файлу требуется не более 12 блоков, любые его данные могут
быть получены за два обращения к диску: одно для чтения элемента массива
ibiock индексного дескриптора на диске, а другое — для чтения запрошен-
запрошенного блока с данными. Для более длинных файлов может потребоваться три
или даже четыре последовательных обращения к диску, чтобы получить
нужный блок данных. На практике это оказывается самой пессимистичной
оценкой, потому что кэши элементов каталога, индексных дескрипторов и
страниц значительно уменьшают количество реальных обращений к диску.
Обратите также внимание на то, как размер блока в файловой системе влияет
на механизм адресации. Большой размер блока позволяет файловой системе
Ext2 хранить больше логических номеров блоков в одном блоке. Табл. 18.11
показывает верхний предел для размера файла при каждом размере блока и
каждом режиме адресации. Например, если размер блока составляет
1024 байта, а файл содержит до 268 Кбайт данных, первые 12 Кбайт файла
доступны при помощи прямой адресации, а остальные килобайты, с 13
по 268, могут быть адресованы с одним уровнем косвенности. Файлы больше
2 Гбайт в 32-битовой архитектуре следует открывать с установленным фла-
флагом O_LARGEFILE.
Таблица 18.11. Верхние пределы для размера файла при разной адресации блоков
Р.,мер6л,Ю [прямая !«—• |«—■ \»— ~~
1024 12 Кбайт 268 Кбайт 64.26 Мбайт 16.06 Гбайт
2048 24 Кбайт 1.02 Мбайт 513.02 Мбайт 256.5 Гбайт
4096 48 Кбайт 4.04 Мбайт 4 Гбайт ~ 4 Тбайт
Дыры в файлах
Дыра в файле — это часть обычного файла, которая содержит нулевые сим-
символы и не хранится ни в каком блоке на диске. Дыры являются давней осо-
особенностью файлов в Unix. Например, следующая команда Unix создает файл,
в котором первые байты являются дырой:
$ echo -n "X" | dd of=/tmp/hole bs=1024 seek=6
Файл /tmp/hole содержит 6145 символов F144 нулевых символа и сим-
символ "Xй), однако, занимает он только один блок диска.
Дыры в файлах были введены, чтобы избежать непроизводительного расхо-
расходования места на диске. Они активно используются приложениями, рабо-
работающими с базами данных, и, вообще, всеми приложениями, выполняющими
хеширование файлов.
Реализация дыр в файлах в Ext2 основана на динамическом выделении бло-
блоков: блок фактически назначается файлу, только когда процессу нужно про-
произвести в него запись. Поле isize каждого индексного дескриптора опреде-
определяет размер файла, видимый программе, включая дыры, в то время как поле
ibiocks хранит количество блоков, фактически выделенных файлу (изме-
(измеренное в единицах по 512 байтов).
Вернувшись к примеру с командой dd, предположим, что файл /tmp/hole был
создан в разделе Ext2, имеющем размер блока 4096. Поле isize индексного
дескриптора на диске содержит число 6145, а поле ibiocks — число 8 (по-
(поскольку блок в 4096 байтов содержит восемь 512-байтовых блоков). Второй
элемент массива ibiock (соответствующий блоку, имеющему в файле но-
номер 1) содержит логический номер выделенного блока, а остальные элементы
этого массива содержат нули (рис. 18.6).
Рис. 18.6. Файл с дырой в начале
Выделение блоков данных
Когда ядру нужно найти блок с данными обычного файла Ext2, оно вызывает
функцию ext2_get_biock (). Если такого блока нет, функция автоматически
выделяет его файлу. Вспомним, что эту функцию можно вызывать каждый
раз, когда ядро выполняет операцию чтения или записи в отношении обычно-
обычного файла Ext2 (см. разд. "Чтение из файла" и "Запись в файл" главы 16). Оче-
Очевидно, что эта функция вызывается, только если требуемый блок отсутствует
в кэше страниц.
Функция ext2_get_biock () работает со структурами данных, описанными ра-
ранее, в разд. "Адресация блоков данных", и, если необходимо, вызывает функ-
функцию ext2_aiioc_biock() для поиска свободного блока в разделе Ext2. Когда
это нужно, функция также выделяет блоки, применяемые для косвенной ад-
адресации (см. рис. 18.5).
Для минимизации фрагментации файла файловая система Ext2 пытается по-
получить для файла новый блок по соседству с последним блоком, выделенным
файлу. Если это ей не удается, файловая система ищет новый блок в группе
блоков, содержащей индексный дескриптор файла. В самом крайнем случае
свободный блок берется из какой-нибудь другой группы.
Файловая система Ext2 применяет предварительное выделение блоков. Файл
получает не только запрошенный блок, а группу, содержащую до восьми
смежных блоков. Поле ipreaiioccount структуры ext2_inode_info содержит
количество блоков, заранее выделенных файлу и все еще неиспользуемых, а
поле ipreaiiocbiock хранит логический номер предварительно выделенно-
выделенного блока, который нужно использовать следующим. Все неиспользованные
предварительно выделенные блоки освобождаются, когда файл закрывается,
когда он усекается, или когда операция записи идет не подряд за операцией
записи, вызвавшей предварительное выделение блоков.
Функция ext2_aiioc_biock() принимает в качестве параметров указатель на
объект "индексный дескриптор" , цель и адрес переменной, которая будет
содержать код ошибки. Цель — это логический номер блока, соответствую-
соответствующий предпочтительному расположению нового блока. Функция ext2_get_
block о задает параметр "цель" в соответствии со следующими эвристиче-
эвристическими правилами:
П Если блок, выделяемый в данный момент, и предыдущий выделенный
блок имеют последовательные номера в файле, целью является логиче-
логический номер предыдущего блока плюс 1. Есть смысл в том, чтобы блоки,
видимые программой как последовательные, были смежными на диске.
□ Если первое правило неприменимо, и хотя бы один блок уже был выделен
файлу, целью является один из логических номеров этих блоков. Точнее
говоря, это логический номер уже выделенного блока, который предшест-
предшествует выделяемому блоку в файле.
□ Если первые два правила неприменимы, целью является логический номер
первого блока (необязательно свободного) в группе блоков, содержащей
индексный дескриптор файла.
Функция ext2_aiioc_biock () проверяет, ссылается ли цель на один из предва-
предварительно выделенных блоков файла. Если это так, функция выделяет этот
блок и возвращает его логический номер. В противном случае она отбрасы-
отбрасывает все оставшиеся предварительно выделенные блоки и вызывает функцию
ext2_new_block().
Эта последняя функция ищет свободный блок в разделе Ext2, применяя сле-
следующую стратегию:
□ Если предпочтительный блок, переданный функции ext2_aiioc_biock()
(то есть блок, являющийся целью) свободен, функция выделяет его.
□ Если цель занята, функция проверяет, свободен ли один из блоков, сле-
следующих за предпочтительным.
□ Если поблизости от предпочтительного блока свободных блоков не оказа-
оказалось, функция рассматривает все группы блоков, начиная с той, которая
содержит цель. Для каждой группы функция выполняет следующие дей-
действия:
• ищет группу из хотя бы восьми смежных свободных блоков;
• если такой группы не окажется, ищет один свободный блок.
Поиск прекращается, как только будет найден свободный блок. Перед завер-
завершением работы функция ext2_new_biock () пытается предварительно выде-
выделить до восьми свободных блоков, смежных с только что найденным блоком,
и записывает в поля ipreaiiocbiock и ipreaiioccount индексного деск-
риптора на диске местоположение блока и количество блоков.
Освобождение блока данных
Когда процесс удаляет файл или усекает его длину до нуля, все блоки файлы
должны быть утилизированы. Это делает функция ext2_truncate (), прини-
принимающая в качестве параметра адрес объекта "индексный дескриптор", соот-
соответствующего файлу. Функция перебирает элементы массива ibiock ин-
индексного дескриптора на диске, чтобы найти все блоки с данными и все бло-
блоки, используемые для косвенной адресации. Затем эти блоки освобождаются
МНОГОКратнЫМИ вызовами фуНКЦИИ ext2_freejolocks ().
Функция ext2_f reebiocks () освобождает группу из одного или нескольких
смежных блоков. Помимо вызова из функции ext2_truncate (), эта функция
вызывается, в основном, при отказе от предварительно выделенных блоков
файла (см. разд. "Выделение блоков данных"ранее в этой главе). Она прини-
принимает следующие параметры:
□ inode — адрес объекта "индексный дескриптор", описывающего файл;
□ block — логический номер первого блока, подлежащего освобождению;
□ count — количество смежных блоков, подлежащих освобождению.
Для каждого освобождаемого блока функция выполняет следующие действия:
1. Получает битовую карту блоков той группы, которая включает освобож-
освобождаемый блок.
2. Обнуляет бит карты, который соответствует освобождаемому блоку, и по-
помечает буфер, содержащий битовую карту, как "грязный".
3. Увеличивает поле bg_free_biocks_count дескриптора группы блоков и по-
помечает соответствующий буфер как "грязный".
4. Увеличивает поле s_f reebiockscount суперблока на диске, помечает со-
соответствующий буфер как "грязный" и устанавливает флаг sdirt объекта-
суперблока.
5. Если файловая система была смонтирована с установленным флагом
mssynchronous, функция вызывает функцию syncdirtybuffero и ждет
окончания операции записи буфера, содержащего битовую карту.
Файловая система Ext3
В этом разделе мы кратко опишем более современную файловую систему,
являющуюся развитием Ext2, а именно Ext3. Разработчики новой файловой
системы придерживались двух простых концепций:
□ она должна быть журналируемой;
□ она должна быть максимально совместима с файловой системой Ext2.
Ext3 прекрасно соответствует этим требованиям. В частности, она основана
на Ext2, и ее структуры данных на диске практически идентичны структурам
файловой системы Ext2. Фактически, если файловая система Ext3 была раз-
размонтирована штатным образом, то ее можно затем смонтировать как файло-
файловую систему Ext2; и наоборот, создание журнала в файловой системе Ext2 и
перемонтирование ее как Ext3 является простой и быстрой операцией.
Благодаря совместимости между Ext3 и Ext2, значительная часть сказанного
в предыдущих разделах этой главы относится также и к Ext3. Поэтому в сле-
следующем разделе мы основное внимание уделим новой функциональной воз-
возможности, предлагаемой файловой системой Ext3 — журналированию.
Журналируемые файловые системы
По мере увеличения емкости дисков, одна конструктивная особенность тра-
традиционных файловых систем Unix (в частности, Ext2) превратилась в недос-
недостаток. Как мы знаем из главы 14, изменения, внесенные в блоки файловой
системы, могут оставаться в динамической памяти довольно долго, пока не
будут записаны на диск. Авария, например, сбой питания или крах системы,
может оставить файловую систему в некорректном состоянии. Для преодоле-
преодоления этой проблемы каждая традиционная файловая система Unix проходит
проверку перед монтированием. Если она не была размонтирована должным
образом, то специальная программа выполняет исчерпывающую и длитель-
длительную проверку, в ходе которой приводит в надлежащее состояние все струк-
структуры файловой системы на диске.
Например, состояние файловой системы Ext2 хранится в поле smountstate
суперблока на диске. Утилита e2fsck вызывается загрузочным скриптом и
проверяет значение этого поля. Если оно не равно ext2_valid_fs, значит,
файловая система не была корректно размонтирована, и e2fsck начинает про-
проверку всех структур файловой системы, хранящихся на диске.
Очевидно, что время, потраченное на проверку непротиворечивости файло-
файловой системы, зависит, в основном, от количества файлов и каталогов, кото-
которые нужно просмотреть. Следовательно, оно также зависит от размера диска.
В настоящее время, когда файловые системы могут занимать сотни гигабайт,
одна проверка непротиворечивости может потребовать несколько часов. Та-
Такие потери времени недопустимы ни в коммерческой системе, ни в сильно
загруженном сервере.
Цель журналируемой файловой системы заключается в том, чтобы избежать
длительных проверок всей файловой системы за счет просмотра специальной
области диска, содержащей результаты самых последних операций записи и
называемой журналом. Повторное монтирование файловой системы с жур-
журналом после сбоя занимает считанные секунды.
Ext3 — журналируемая файловая система
Идея, лежащая в основе ведения журнала в Ext3, состоит в том, что каждое
изменение в файловой системе, вносимое на высоком уровне, выполняется за
два шага. Вначале копии всех блоков, подлежащих записи, сохраняются в
журнале; затем, когда операция записи в журнал завершится (иными словами,
данные зафиксированы в журнале), блоки сохраняются в файловой системе.
После завершения операции записи в файловую систему (данные зафиксиро-
зафиксированы в файловой системе) копии блоков в журнале уничтожаются.
При восстановлении после сбоя программа e2fsck различает следующие два
случая:
□ Сбой системы произошел до фиксирования в журнале. Либо копии бло-
блоков, содержащих изменения высокого уровня, отсутствуют в журнале, ли-
либо они неполны. В обоих случаях e2fsck игнорирует их.
□ Сбой системы произошел после фиксирования в журнале. Копии блоков
корректны, и e2fsck записывает их в файловую систему.
В первом случае изменения, которые должны быть внесены в файловую сис-
систему, теряются, но ее состояние остается непротиворечивым. Во втором слу-
случае e2fsck вносит необходимые изменения, тем самым исправляя противоре-
противоречивость, возникшую в результате незавершенной операции пересылки дан-
данных.
Не следует ожидать от журналируемой файловой системы слишком многого.
Она гарантирует непротиворечивость только на уровне системных вызовов.
Например, сбой операционной системы, возникший во время копирования
большого файла с помощью системных вызовов write (), прервет операцию
копирования, и копия окажется короче оригинала.
Более того, журналируемые файловые системы обычно не копируют все бло-
блоки в журнал. На самом деле, каждая файловая система состоит из блоков двух
типов: содержащих метаданные и содержащих обычные данные. В файловых
системах Ext2 и Ext3 существует шесть видов метаданных: суперблоки, деск-
дескрипторы групп блоков, индексные дескрипторы, блоки косвенной адресации
(косвенные блоки), блоки с битовыми картами блоков данных и блоки с би-
битовыми картами индексных дескрипторов. Другие файловые системы приме-
применяют собственные типы метаданных.
Некоторые журналируемые файловые системы, такие как SGI XFS и IBM
JFS, ограничиваются тем, что записывают в журнал только операции, затра-
затрагивающие метаданные. Конечно, записи метаданных в журнал достаточно
для восстановления структур файловой системы, хранящихся на диске. Одна-
Однако, поскольку операции над блоками, содержащие данные файлов, в журнале
не регистрируются, ничто не мешает сбою повредить содержимое файлов.
Файловую систему Ext3 можно сконфигурировать так, чтобы она записывала
в журнал операции, затрагивающие как метаданные файловой системы, так и
блоки с данными файлов. Поскольку регистрация в журнале каждой опера-
операции записи ведет к значительному снижению производительности, Ext3 по-
позволяет системному администратору решать, что будет заноситься в журнал.
В частности, она предлагает три режима ведения журнала:
□ Журналирование — все изменения данных и метаданных файловой систе-
системы записываются в журнал. Этот режим минимизирует вероятность поте-
потери изменений в файле, но требует много дополнительных обращений к
диску. Например, когда создается новый файл, все его блоки с данными
должны быть продублированы в журнале. Это самый безопасный и самый
медленный режим ведения журнала в Ext3.
□ Упорядоченный — в журнал записываются только изменения в метадан-
метаданных файловой системы. Однако файловая система Ext3 группирует мета-
метаданные и соответствующие блоки данных так, что обычные данные запи-
записываются на диск раньше метаданных. Таким образом, вероятность порчи
данных в файлах снижается. Например, каждая операция записи, увеличи-
увеличивающая длину файла, гарантированно защищена журналом. Этот режим
ведения журнала устанавливается в Ext2 по умолчанию.
□ Запись на диск — в журнал записываются только изменения в метаданных
файловой системы. Так поступают другие файловые системы с журналом,
и это самый быстрый режим.
Режим ведения журнала в файловой системе Ext3 задается опцией команды
mount. Например, чтобы смонтировать файловую систему, которая хранится
в разделе /dev/sda2, на точке монтирования /jdisk с режимом ведения журна-
журнала "Запись на диск", системный администратор может ввести следующую
команду:
# mount -t ext3 -о data=writeback /dev/sda2 /jdisk
Слой журналирующего блочного устройства
Журнал Ext3 обычно хранится в скрытом файле по имени journal, располо-
расположенном в корневом каталоге файловой системы.
Файловая система Ext3 сама журнал не ведет. Она пользуется общим слоем
ядра, называемым журналирующим блочным устройством. В настоящее
время только Ext3 прибегает к услугам этого поля, но в будущем его, воз-
возможно, станут использовать и другие файловые системы.
Слой журналирующего блочного устройства является довольно сложным
программным компонентом. Файловая система Ext3 вызывает его процедуры,
чтобы гарантировать, что ее последующие операции не разрушат структуры
данных на диске в случае сбоя. Однако в типичном случае этот слой ведет
журнал на том же диске и, следовательно, уязвим перед сбоями в той же сте-
степени, что и Ext3. Другими словами, слой блочного устройства, ведущего
журнал, сам должен защищаться от сбоев системы, которые могут повредить
журналу.
Взаимодействие между Ext3 и журналирующим блочным устройством осно-
основано на трех фундаментальных единицах:
□ запись журнала — записывает одно обновление блока на диске в журна-
лируемой файловой системе;
□ пакет атомарных операций— включает в себя записи журнала, относя-
относящиеся к единичной операции высокого уровня, выполненной в файловой
системе. Как правило, каждый системный вызов, модифицирующий фай-
файловую систему, приводит к выполнению одного пакета атомарных опе-
операций;
□ транзакция — включает в себя несколько пакетов атомарных операций, у
которых записи журнала были одновременно помечены как корректные
для программы e2fsck.
Записи журнала
Запись журнала — это, в сущности, описание операции низкого уровня, ко-
которая будет выполнена в файловой системе. В некоторых журналируемых
файловых системах такая запись состоит из последовательности байтов, ко-
которые были модифицированы, и их начальной позиции в файловой системе.
Однако слой блочного устройства, ведущего журнал, использует журнальные
записи, состоящие из целого буфера, модифицированного операцией низкого
уровня. Такой подход может привести к крайне неэкономному расходованию
пространства журнала (например, когда операция низкого уровня изменяет
один бит в битовой карте), но он очень быстр, поскольку слой может рабо-
работать непосредственно с буферами и их головами.
Таким образом, записи журнала представлены в нем как обычные блоки дан-
данных (или метаданных). Каждый такой блок ассоциирован с небольшим тегом
типа journaibiocktagt, который содержит логический номер блока в фай-
файловой системе и несколько флагов состояния.
Впоследствии, когда слой журналирующего блочного устройства изучает
буфер, либо потому что он принадлежит записи журнала, либо потому что
это блок данных должен быть записан на диск до соответствующего блока с
метаданными (в режиме ведения журнала "Упорядоченный"), ядро прикреп-
прикрепляет Структуру journalhead К ГОЛОВе буфера. В ЭТОМ Случае ПОЛе b_private
головы буфера содержит адрес структуры journalhead и установленный
флаг bhjbd (см. разд. "Блочные буферы и головы буферов" главы 15).
Пакеты атомарных операций
Каждый системный вызов, модифицирующий файловую систему, обычно
разбит на ряд операций низкого уровня, которые манипулируют со структу-
структурами данных на диске.
Предположим, например, что файловая система Ext3 должна удовлетворить
запрос пользователя на добавление блока данных к обычному файлу. Слой
файловой системы должен определить последний блок файла, найти свобод-
ный блок в файловой системе, обновить битовую карту блоков данных в со-
соответствующей группе блоков, сохранить логический номер нового блока в
индексном дескрипторе файла или в блоке косвенной адресации, записать
содержимое нового блока и, наконец, обновить несколько полей индексного
дескриптора. Как видите, операция добавления данных преобразуется во
множество операций низкого уровня над блоками данных и метаданных фай-
файловой системы.
Теперь только представьте, что может произойти, если сбой системы случит-
случится в середине операции добавления данных, когда одни операции низкого
уровня уже завершены, а остальные еще нет. Сценарий может быть еще хуже,
если операция высокого уровня затрагивает несколько файлов (например,
при переносе файла из одного каталога в другой).
Чтобы предотвратить порчу данных, файловая система Ext3 должна гаранти-
гарантировать, что каждый системный вызов обрабатывается атомарным образом.
Пакет атомарных операций— это набор операций низкого уровня над
структурами на диске, соответствующих одной операции высокого уровня.
При восстановлении после сбоя файловая система гарантирует, что либо вся
операция высокого уровня выполнена, либо не выполнена ни одна операция
низкого уровня.
Каждый пакет атомарных операций представлен дескриптором, имеющим
тип handiet. Чтобы начать атомарную операцию, файловая система Ext3
вызывает функцию слоя журналирующего блочного устройства, journai_
start (), которая выделяет, если необходимо, новый пакет атомарных опера-
операций и заносит его в текущие транзакции (см. следующий раздел). Поскольку
любая дисковая операция низкого уровня может приостановить выполнение
процесса, адрес активного пакета хранится в поле journaiinfo дескриптора
процесса. Для уведомления о завершении атомарной операции файловая сис-
система Ext3 ВЫЗЫВаеТ фуНКЦИЮ journal_stop () .
Транзакции
По соображениям эффективности слой журналирующего блочного устройст-
устройства работает с журналом, группируя его записи, принадлежащие нескольким
пакетам атомарных операций, в транзакцию. Более того, все журнальные
записи, имеющие отношение к пакету, должны быть включены в одну транз-
транзакцию.
Все записи журнала, принадлежащие одной транзакции, хранятся в последо-
последовательных блоках журнала. Слой журналирующего блочного устройства вы-
выполняет каждую транзакцию как единое целое. Например, он утилизирует
блоки, используемые транзакцией, только после того, как все данные, содер-
содержащиеся в ее журнальных записях, зафиксированы в файловой системе.
Сразу после своего создания транзакция может принимать журнальные запи-
записи новых пакетов. Транзакция прекращает прием новых пакетов, когда воз-
возникнет одна из следующий ситуаций:
□ прошел определенный период времени (как правило, 5 секунд);
□ в журнале не осталось свободных блоков для нового пакета.
Транзакция представлена дескриптором, имеющим тип transactiont. Самым
важным его полем является tstate, которое описывает текущее состояние
транзакции.
Вообще говоря, транзакция может быть:
□ завершенной — все записи журнала, включенные в транзакцию, были фи-
физически записаны в журнал. При восстановлении после сбоя программа
e2fsck рассматривает каждую завершенную транзакцию журнала и запи-
записывает соответствующие блоки в файловую систему. В этом случае поле
t_state содержит значение t_finished;
П незавершенной— хотя бы одна запись журнала, включенная в транзак-
транзакцию, не была физически записана в журнал, либо новые записи журнала
все еще добавляются в транзакцию. В случае сбоя системы образ транзак-
транзакции, хранящийся в журнале, скорее всего, будет устаревшим. Следова-
Следовательно, при восстановлении после сбоя программа e2fsck не доверяет не-
незавершенным транзакциям в журнале и пропускает их. В этом случае поле
tstate содержит одно из следующих значений:
• trunning — транзакция все еще принимает новые пакеты атомарных
операций;
• tlocked — транзакция не принимает новые пакеты атомарных опера-
операций, но некоторые из них еще не завершены;
• tflush— все пакеты атомарных операций завершены, но некоторые
записи еще заносятся в журнал;
• tcommit— все журнальные записи пакетов атомарных операций со-
сохранены на диске, но транзакцию еще следует пометить как завершен-
завершенную в журнале.
В любой момент журнал может содержать несколько транзакций, но только
одна из них находится в состоянии trunning, т. е. является активной тран-
транзакцией, которая принимает новые запросы на пакеты атомарных операций,
выдаваемые файловой системой.
Несколько транзакций в журнале могут быть незавершенными, поскольку
буферы, содержащие соответствующие записи журнала, могут быть еще не
переписаны в журнал.
Когда транзакция завершена, все ее журнальные записи уже находятся в
журнале, но некоторые из соответствующих буферов еще должны быть со-
сохранены в файловой системе. Завершенная транзакция удаляется из журнала,
когда слой журналирующего блочного устройства убеждается, что все буфе-
буферы, имеющие отношение к записям журнала, были успешно сохранены в
файловой системе Ext3.
Как ведется журнал
В этом разделе мы на примере продемонстрируем, как ведется журнал. Пред-
Предположим, слой файловой системы Ext3 принимает запрос на запись несколь-
нескольких блоков обычного файла.
Как нетрудно догадаться, мы не собираемся подробно описывать каждую
операцию слоя файловой системы Ext3 и слоя блочного устройства, ведущего
журнал. Нам пришлось бы затронуть слишком много моментов! Мы просто
опишем самые важные действия:
1. Служебная процедура системного вызова write о запускает метод write
файлового объекта, ассоциированного с обычным файлом Ext3. В файло-
файловой системе Ext3 ЭТОТ метод реализован функцией generic_file_write(),
которая была описана в разд. "Запись в файл" в главе 16.
2. Функция generic_fiie_write() несколько раз вызывает метод
preparewrite объекта addressspace, ПО разу ДЛЯ каждой страницы дан-
ных, вовлеченной в операцию записи. В Ext3 этот метод реализован функ-
функцией ext3_prepare_write().
3. Функция ext3_prepare_write() запускает новую атомарную операцию, вы-
вызвав функцию journaistarto слоя журналирующего блочного устройст-
устройства. Пакет добавляется к активной транзакции. На самом деле, пакет ато-
атомарных операций создается только при первом вызове функции
journaistart (). При следующих вызовах функция убеждается, что поле
journaiinfo дескриптора процесса уже содержит осмысленную информа-
информацию, и обращается к пакету, на который ссылается поле.
4. Функция ext3_prepare_write() вызывает функцию block_prepare_write (),
описанную в главе 16, передавая ей адрес функции ext3_get_biock ().
Вспомним, что функция biockpreparewrite() отвечает за подготовку
буферов и их голов для страницы файла.
5. Когда ядро должно определить логический номер блока в файловой сис-
системе Ext3, оно выполняет функцию ext3_get_biock (). Эта функция анало-
аналогична функции ext2_get_biock (), описанной в разд. "Выделение блока
данных" ранее в этой главе. Однако важное отличие файловой системы
Ext3 состоит в том, что она вызывает функции слоя журналирующего
блочного устройства, чтобы гарантировать занесение в журнал операций
низкого уровня:
• до запуска низкоуровневой операции записи блока метаданных фай-
файловой СИСТемы функция вызывает функцию ext3_prepare_write ().
В принципе, вызванная функция добавляет буфер с метаданными в
список активной транзакции, но она также должна проверить, включе-
включены ли эти метаданные в какую-нибудь старую незавершенную транзак-
транзакцию журнала. Если это так, функция создает копию буфера, чтобы ста-
старые транзакции выполнялись со старым содержимым;
• после обновления буфера с блоком метаданных файловая система Ext3
вызывает функцию journaigetwriteaccess о, чтобы перенести буфер
с метаданными в список "грязных" буферов транзакции и записать опе-
операцию в журнал.
Обратите внимание, что буферы с метаданными, обрабатываемые слоем
журналирующего блочного устройства, обычно не содержатся в списках
"грязных" буферов индексного дескриптора и поэтому не записываются на
диск обычными механизмами записи кэша, обсуждавшимися в главе 15.
6. Если файловая система Ext3 была смонтирована для работы в режиме
"Журнал", функция ext3_prepare_write () вызывает также функцию
journai_get_write_access() для каждого буфера, затронутого операцией
записи.
7. Управление возвращается функции genericfiiewriteO, которая обнов-
обновляет страницу с данными, хранящуюся в адресном пространстве режима
пользователя, а затем ВЫЗЫВает метод commit_write объекта address_space.
В файловой системе Ext3 выбор функции, реализующей этот метод, зави-
зависит от того, как была смонтирована Ext3:
• если файловая система Ext3 была смонтирована в режиме "Журнал",
метод commit_write реализуется функцией ext3_journalled_commit_
write (), КОТОрая ВЫЗЫВает функцию journal_dirty_metadata() ДЛЯ каж-
дого буфера данных (не метаданных) на странице. Таким образом, бу-
буфер включается в список "грязных" буферов активной транзакции, а не
в список индексного дескриптора; при этом соответствующие жур-
журнальные записи заносятся в журнал. В конце работы функция
ext3_j ournalled_commit_write () вызывает функцию j ournal_stop (),
чтобы уведомить слой блочного устройства, ведущего журнал, о закры-
закрытии пакета атомарных операций;
• если файловая система Ext3 была смонтирована в режиме "Упорядо-
"Упорядоченный", метод coramitwrite реализуется функцией ext3_ordered_
commitwrite (), КОТОрая ВЫЗЫВает функцию journal_dirty_data() ДЛЯ
каждого буфера данных на странице, чтобы включить его в соответст-
вующий список активной транзакции. Слой журналирующего блочно-
блочного устройства гарантирует, что все буферы в этом списке записывают-
записываются на диск до буферов с метаданными, включенных в транзакцию. Ни-
Никакие записи в журнал не заносятся. Далее функция ext3_ordered_
coramit_write () вызывает обычную функцию generic_commit_write ()
(описанную в главе 75), которая добавляет буферы с данными в список
"грязных" буферов их владельца, т. е. индексного дескриптора. В кон-
конце работы фунКЦИЯ ext3_ordered_commit_write() ВЫЗЫВаеТ функцию
journaistopo, чтобы уведомить слой блочного устройства, ведущего
журнал, о закрытии пакета атомарных операций;
• если файловая система Ext3 была смонтирована в режиме "Запись на
ДИСК", метод coramitwrite реализуется функцией ext3_writeback_
commitwrite (), вызывающей обычную функцию generic_commit_
write () (описанную в главе 75), которая добавляет буферы с данными
в список "грязных" буферов их владельца, т. е. индексного дескрипто-
дескриптора. Затем функция ext3_writeback_coramit_write() вызывает функцию
journaistopo, чтобы уведомить слой блочного устройства, ведущего
журнал, о закрытии пакета атомарных операций.
8. Здесь служебная процедура системного вызова write о заканчивает свою
работу. Однако этого нельзя сказать про слой журналирующего блочного
устройства. Через какое-то время журнальные записи нашей транзакции
будут физически занесены в журнал, и она станет завершенной. Тогда
будет вызвана функция journal_commit_transaction ().
9. Если файловая система Ext3 была смонтирована в режиме "Упорядочен-
"Упорядоченный", функция journal_commit_transaction() Запускает Операцию ВВО-
да/вывода для всех буферов, включенных в список транзакции, и ждет за-
завершения пересылки данных.
10. Функция journaicommittransactiono запускает операцию ввода/вывода
для всех буферов с метаданными, включенных в список транзакции
(а также для всех буферов с данными, если Ext3 была смонтирована в ре-
режиме "Журнал").
11. Периодически ядро активизирует проверку для каждой завершенной
транзакции в журнале. Эта деятельность, в основном, сводится к провер-
проверке того, что все операции ввода/вывода, запущенные функцией
journaicommittransactiono, ЗакОНЧИЛИСЬ успешно. ЕСЛИ ЭТО так, тран-
закцию можно удалять из журнала.
Конечно, записи в журнале не имеют особого значения, пока не случится
сбой системы. Только при рестарте системы программа e2fsck будет про-
просматривать журнал файловой системы и заново запускать описанные в нем
операции записи на диск, чтобы завершить транзакции.
ГЛАВА 19
Взаимодействие процессов
В этой главе описано, как процессы режима пользователя могут синхронизи-
синхронизировать свои действия и обмениваться данными. Мы уже затронули некоторые
вопросы синхронизации в главе 5, но тогда действующими лицами были
управляющие тракты ядра, а не пользовательские программы. Теперь, после
подробного обсуждения файловых систем и управления вводом/выводом, мы
готовы продолжить разговор о процессах режима пользователя. Эти процес-
процессы не могут обойтись без поддержки ядра при решении вопросов взаимодей-
взаимодействия и синхронизации.
Как было сказано в главе 12, одной из форм синхронизации процессов в ре-
режиме пользователя является создание файла (возможно, пустого) и его бло-
блокирование/разблокирование при помощи системных вызовов виртуальной
файловой системы VFS. В то время как совместное использование данных
процессами может быть аналогичным образом реализовано с помощью вре-
временных файлов, защищаемых блокировками, такой подход обойдется дорого,
поскольку он требует обращения к дисковой файловой системе. По этой при-
причине в каждом ядре Unix имеется набор системных вызовов, поддерживаю-
поддерживающих взаимодействие процессов без общения с файловой системой. Кроме то-
того, было разработано и включено в соответствующие библиотеки несколько
интерфейсных функций, которые позволяют процессам выдавать ядру запро-
запросы на синхронизацию.
Как всегда, разработчики приложений нуждаются в самых разных механиз-
механизмах межпроцессного взаимодействия. Перечислим основные такие механиз-
механизмы, предлагаемые системами Unix:
□ каналы и FIFO-файлы (именованные каналы) — они лучше всего подхо-
подходят для взаимодействия между процессами по схеме "производитель —
потребитель". Некоторые процессы заполняют канал данными, а другие —
извлекают данные из канала. Эти механизмы обсуждаются в разд. "Ка-
"Каналы " и "FIFO-файлы ";
□ семафоры — как можно догадаться по названию, они представляют собой
версию семафоров ядра (описанных в главе 5), реализованную в режиме
пользователя. Они обсуждаются в разд. "Схема межпроцессного взаимо-
взаимодействия System VIPC";
□ сообщения — процессы имеют возможность обмениваться сообщениями
(короткими блоками данных), записывая их в специальные очереди и чи-
читая оттуда. Ядро Linux предлагает две версии сообщений: сообщения
System V IPC и сообщения POSIX (описанные в разд. "Очереди сообщений
POSIX");
□ совместно используемые области памяти — они позволяют процессам об-
обмениваться информацией через блоки памяти. Для приложений, которые
должны совместно использовать большие объемы данных, это самая эф-
эффективная форма взаимодействия. Этот механизм описан в разд. "Схема
межпроцессного взаимодействия System VIPC";
□ сокеты— они позволяют процессам, работающим на разных компьюте-
компьютерах, обмениваться данными через сеть. Сокеты могут быть также исполь-
использованы как инструмент взаимодействия и процессами, работающими на
одном компьютере. Например, графический интерфейс X Window System
пользуется сокетом, чтобы позволить клиентским программам обмени-
обмениваться данными с Х-сервером.
Каналы
Каналы являются механизмом межпроцессного взаимодействия, который
есть во всех Unix-подобных системах. Канал — это однонаправленный поток
данных между процессами. Все данные, записываемые в канал одним про-
процессом, ядро направляет другому процессу, который может их прочитать.
В командных оболочках Unix каналы можно создавать с помощью операто-
оператора |. Например, следующая команда заставляет оболочку создать два процес-
процесса, соединенных каналом:
$ Is I more
Стандартный поток вывода первого процесса, выполняющего программу is,
направляется в канал, а второй процесс, выполняющий программу more, чи-
читает свои входные данные из канала.
Обратите внимание, что того же результата можно добиться двумя команда-
командами, например, такими:
$ Is > temp
$ more < temp
Первая команда перенаправляет вывод программы is в обычный файл, а вто-
вторая заставляет программу тоге читать из этого файла. Конечно, применение
каналов вместо временных файлов гораздо удобнее по следующим причинам:
О команда оболочки короче и проще;
□ нет необходимости создавать временные файлы, которые потом приходит-
приходится удалять.
Работа с каналом
Каналы можно считать открытыми файлами, не отраженными в смонтиро-
смонтированных файловых системах. Процесс создает новый канал при помощи сис-
системного вызова pipe о, который возвращает пару файловых дескрипторов.
Процесс может впоследствии передавать эти дескрипторы своим потомкам
через системный вызов fork о и пользоваться каналом вместе с ними. Про-
Процессы могут читать данные из канала, делая системный вызов read () с пер-
первым файловым дескриптором. Аналогично, они могут записывать информа-
информацию в канал, делая системный вызов write () со вторым файловым дескрип-
дескриптором.
Стандарт POSIX определяет только полудуплексные каналы, поэтому, хотя
системный вызов pipe о и возвращает два файловых дескриптора, каждый
процесс должен закрыть один из них прежде, чем воспользуется другим. Ес-
Если нужен двухсторонний поток данных, процесс должен создать два разных
канала, сделав системный вызов pipe () дважды.
В некоторых Unix-подобных системах, например System V Release 4, реали-
реализованы полнодуплексные каналы. У полнодуплексного канала оба дескрипто-
дескриптора могут быть использованы как для чтения, так и для записи, что создает два
двунаправленных информационных потока. В Linux принят еще один подход:
файловые дескрипторы каждого канала являются однонаправленными, но
закрывать один из них перед использованием другого не требуется.
Вернемся к предыдущему примеру. Когда командная оболочка интерпрети-
интерпретирует оператор is imore, она выполняет следующие действия:
1. Делает системный вызов pipe (). Предположим, он возвратил файловые
дескрипторы 3 (канал чтения) и 4 (канал записи).
2. Два раза делает системный вызов fork ().
3. Два раза делает системный вызов close о, чтобы освободить файловые
дескрипторы 3 и 4.
Первый процесс-потомок, который должен выполнять программу Is, произ-
производит следующие действия:
1. Делает системный вызов dup2D,i), чтобы скопировать файловый деск-
дескриптор 4 в файловый дескриптор 1. С этого момента файловый дескрип-
дескриптор 1 ссылается на канал записи.
2. Два раза делает системный вызов close (), чтобы освободить файловые де-
дескрипторы 3 и 4.
3. Делает системный вызов execveo, чтобы выполнить программу Is (см.
разд. "Функции exec" главы 20). Программа пытается записывать свои вы-
выходные данные в файл, имеющий дескриптор 1 (стандартный вывод), т. е.
записывает их в канал.
Второй процесс-потомок выполняет программу more и поэтому производит
следующие действия:
1. Делает системный вызов dup2 C,0), чтобы скопировать файловый деск-
дескриптор 3 в файловый дескриптор 0. С этого момента файловый дескрип-
дескриптор 0 ссылается на канал чтения.
2. Два раза делает системный вызов close (), чтобы освободить файловые де-
дескрипторы 3 и 4.
3. Делает системный вызов execveo, чтобы выполнить программу more. По
умолчанию она читает свои входные данные из файла, имеющего деск-
дескриптор 0 (стандартный ввод), т. е. из канала.
В этом простом примере каналом пользуются ровно два процесса. Однако,
благодаря своей реализации, канал может быть использован произвольным
количеством процессов1. Очевидно, что если два или больше процессов чи-
читают один канал или пишут в него, они должны явным образом синхронизи-
синхронизировать свои обращения к нему с помощью блокировки (см. разд. "Блокировка
файлов в Linux" главы 12) или семафоров (о них далее в этой главе).
Многие Unix-системы в дополнение к системному вызову pipe () предостав-
предоставляют две интерфейсные функции: рореп () и pciose (), которые делают всю
"черную" работу, касающуюся каналов. Если канал создается с помощью
функции рореп(), к нему можно обращаться из высокоуровневых функций
ввода/вывода, включенных в библиотеку С (fprintf (), f scanf () и др.)
В Linux функции рореп о и pciose о включены в библиотеку С. Функция
рореп о принимает два параметра: filename — путь к выполняемому файлу и
type— строку, определяющую направление движения данных. Она возвра-
возвращает указатель на структуру file.
1 Поскольку большинство оболочек предлагает каналы, соединяющие только два процесса, прило-
приложения, в которых каналы используются более чем двумя процессами, должны быть написаны на
языке программирования, например, С.
Функция рореп () выполняет следующие действия:
1. Создает новый канал с помощью системного вызова pipe ()
2. Ответвляет новый процесс, который, со своей стороны, выполняет сле-
следующие действия:
• если параметр type равен г, процесс копирует файловый дескриптор,
ассоциированный с каналом записи, в файловый дескриптор 1 (стан-
(стандартный вывод). Если же параметр type равен w, процесс копирует фай-
файловый дескриптор, ассоциированный с каналом чтения, в файловый де-
дескриптор 0 (стандартный ввод);
• закрывает файловые дескрипторы, возвращенные системным вызовом
pipe();
• делает системный вызов execve (), чтобы выполнить программу, опре-
определяемую параметром filename.
3. Если параметр type равен г, процесс закрывает файловый дескриптор, ас-
ассоциированный с каналом записи; если же этот параметр равен w, процесс
закрывает файловый дескриптор, ассоциированный с каналом чтения.
4. Возвращает адрес указателя file, который ссылается на оставшийся от-
открытым файловый дескриптор для канала.
После выполнения функции рореп () родитель и потомок могут обмениваться
информацией через канал. Процесс-родитель может читать (если параметр
type равен г) или записывать (если параметр type равен w) информацию,
пользуясь указателем file, возвращенным функцией. Данные записываются в
стандартный вывод или, соответственно, считываются со стандартного ввода
программой, выполняемой процессом-потомком.
Функция pciose () (которая в качестве параметра принимает указатель, воз-
возвращенный функцией рореп о) просто делает системный вызов wait4() и
ждет окончания процесса, созданного функцией рореп ().
Структуры данных для канала
Сейчас нам опять придется мыслить на уровне системных вызовов. После
того, как канал создан, процесс обращается к нему, пользуясь системными
вызовами виртуальной файловой системы, а именно read () и write (). Поэто-
Поэтому для каждого канала ядро создает объект "индексный дескриптор" плюс
два файловых объекта — один для чтения, а другой для записи. Когда про-
процессу понадобится читать из канала или записывать в него, он воспользуется
соответствующим файловым дескриптором.
Когда индексный дескриптор ссылается на канал, его поле ipipe указывает
на структуру pipeinodeinfo, описанную в табл. 19.1.
Таблица 19.1. Структура pipe_inode_infо
Тип Поле Описание
struct wait_queue * wait Очередь ожидания канала
или FIFO-файла
unsigned int nrbuf s Количество буферов, содержащих
данные для чтения
unsigned int curbuf Индекс первого буфера, содержаще-
содержащего данные для чтения
struct pipe_buffer [16] bufs Массив из дескрипторов буферов
канала
struct page * tmp_page Указатель на кэшированный стра-
страничный кадр
unsigned int start Позиция чтения в текущем буфере
канала
unsigned int readers Флаг для читающих процессов
(или их количество)
unsigned int writers Флаг для пишущих процессов
(или их количество)
unsigned int waiting_writers Количество пишущих процессов,
ожидающих в очереди
unsigned int r_counter Аналогично полю readers, но ис-
используется при ожидании процесса,
который читает из FIFO-файла
unsigned int w_counter Аналогично полю writers, но ис-
используется при ожидании процесса,
который пишет в FIFO-файл
struct fasync_struct * fasync_readers Применяется при уведомлениях
асинхронного ввода/вывода
с помощью сигналов
struct f async_struct * f async_writers Применяется при уведомлениях
асинхронного ввода/вывода
с помощью сигналов
Помимо одного индексного дескриптора и двух файловых объектов, у каждо-
каждого канала есть собственный набор буферов. В сущности, буфер канала — это
страничный кадр, содержащий данные, которые записаны в канал и должны
быть прочитаны из него. До версии Linux 2.6.10 у каждого канала был только
один буфер. В версии 2.6.10 буферизация данных для каналов (и FIFO-
файлов) была серьезно пересмотрена, и теперь у каждого канала может быть
до 16 буферов. Это усовершенствование значительно увеличило производи-
тельность приложений режима пользователя, записывающих в канал боль-
большие объемы данных.
Поле bufs структуры pipeinodeinfo содержит массив из 16 объектов
pipebuf f ег, каждый из которых описывает буфер канала. Поля такого объек-
объекта перечислены в табл. 19.2.
Таблица 19.2. Поля объекта pipe_buffer
Тип Поле Описание
struct page * page Адрес дескриптора страничного кадра для
буфера канала
unsigned int offset Текущая позиция значимых данных внутри
страничного кадра
unsigned int len Длина значимых данных в буфере канала
struct pipe_buf_operations * ops Адрес таблицы методов, относящихся
к буферу канала (null, если буфер пуст)
Поле ops указывает на таблицу anonpipebufops методов буфера канала, что
является структурой типа pipebufoperations. Эта таблица включает в себя
три метода:
□ тар — вызывается перед обращением к данным, хранящимся в буфере ка-
канала. Метод вызывает функцию kmap () для страничного кадра канального
буфера на тот случай, если буфер хранится в верхней памяти (см. главу 8);
П unmap — вызывается, когда обращение к данным в буфере закончено. Ме-
Метод вызывает функцию kunmap () для страничного кадра буфера канала;
□ release — вызывается при освобождении буфера канала. Этот метод реа-
реализует одностраничный кэш памяти: освобождается не страничный кадр,
содержащий буфер, а кэшированный страничный кадр, на который указы-
указывает ПОЛе tmppage Структуры pipeinodeinf о (еСЛИ ОНО не Содержит NULL).
Страничный кадр, который хранит буфер, становится новым кэширован-
ным страничным кадром.
Шестнадцать канальных буферов можно рассматривать как глобальный цик-
циклический буфер: пишущие процессы постоянно добавляют данные в этот
большой буфер, а читающие — удаляют их. Количество байтов, записанных
во все канальные буферы, но еще не прочитанных, называется размером ка-
канала. По соображениям эффективности данные, которые еще должны быть
прочитаны, могут быть разбросаны по нескольким частично заполненным
буферам. На самом деле, каждая операция записи может скопировать данные
в свежий, свободный буфер канала, если в предыдущем буфере недостаточно
свободного места для новых данных. Поэтому ядру приходится следить:
□ за буфером канала, содержащим следующий байт, который должен быть
прочитан, и за соответствующим смещением внутри страничного кадра;
индекс этого буфера канала хранится в поле curbuf структуры
pipeinodeinf о, а смещение — в поле offset соответствующего объекта
pipe_buffer;
□ за первым пустым буфером канала. Его индекс может быть вычислен сло-
сложением (по модулю 16) индекса текущего буфера канала, хранящегося в
поле curbuf структуры pipeinodeinfo, и количества буферов канала со
значимыми данными, которое хранится в поле nrbuf s.
Чтобы избежать конфликтов одновременного обращения к структурам кана-
канала, ядро применяет семафор i_sem, входящий в состав объекта "индексный
дескриптор".
Специальная файловая система pipefs
Канал реализован как набор объектов виртуальной файловой системы, у ко-
которых нет образов на диске. В Linux 2.6 эти объекты виртуальной файловой
системы организованы в специальную файловую систему pipefs с целью об-
облегчения работы с ними (см. главу 12). Поскольку у этой файловой системы
нет точки монтирования в системном дереве каталогов, пользователи ее не
видят. Однако, благодаря файловой системе pipefs, каналы полностью интег-
интегрированы в слой VFS, и ядро может работать с ними, как работает с имено-
именованными каналами, FIFO-файлами, которые реально существуют в виде фай-
файлов и видны конечным пользователям (см. разд. "FIFO-файлы " далее в этой
главе).
Функция initpipef s (), обычно выполняемая на этапе инициализации ядра,
регистрирует файловую систему pipefs и монтирует ее (см. разд. "Монтиро-
"Монтирование типичной файловой системы " гл. 12):
struct file_system_type pipe_fs_type;
pipe_fs_type.name = "pipefs";
pipe_f s_type. get_sb = pipefs_get_sb;
pipe fs.kill sb = kill anon_super;
register_filesystem(&pipe_fs_type) ;
pipe_mnt = do_kern_mount("pipefs", 0, "pipefs", NULL);
Объект "смонтированная файловая система", представляющий корневой ка-
каталог файловой системы pipefs, хранится в переменной pipemnt.
Создание и уничтожение канала
Системный вызов pipe () обслуживается функцией syspipe (), которая вызы-
вызывает функцию dopipe (). Чтобы создать новый канал, функция dopipe () вы-
выполняет следующие действия:
1. Вызывает функцию getpipeinode (), которая выделяет и инициализирует
индексный дескриптор для канала в файловой системе pipefs. В частности,
эта функция делает следующее:
• выделяет новый индексный дескриптор для канала в файловой системе
pipefs;
• выделяет структуру pipeinodeinf о и сохраняет ее адрес в поле ipipe
индексного дескриптора;
• обнуляет ПОЛЯ curbuf И nrbuf s Структуры pipeinodeinf о. Кроме ТОГО,
заполняет нулями все поля объектов "буфер канала" в массиве buf s;
• инициализирует единицами поля rcounter и wcounter структуры
pipe_inode_info;
• инициализирует единицами поля readers и writers структуры
pipe_inode_info.
2. Выделяет файловый объект и файловый дескриптор для канала чтения,
устанавливает поле f_f lag файлового объекта в значение ordonly и ини-
инициализирует поле f op адресом таблицы readpipefops.
3. Выделяет файловый объект и файловый дескриптор для канала записи,
устанавливает поле f_f lag файлового объекта в значение owronly и ини-
инициализирует поле fop адресом таблицы write_ pipefops.
4. Выделяет объект "элемент каталога" и использует его для связи двух фай-
файловых объектов и объекта "индексный дескриптор" (см. разд. "Общая
файловая модель" главы 12). Затем включает новый индексный дескрип-
дескриптор в специальную файловую систему pipefs.
5. Возвращает процессу режима пользователя два файловых дескриптора.
Процесс, сделавший системный вызов pipe (), поначалу является единствен-
единственным процессом, имеющим доступ к новому каналу, как для чтения, так и для
записи. Чтобы отразить тот факт, что у канала есть читающий процесс и пи-
пишущий процесс ПОЛЯ readers И writers структуры pipeinodeinfo ИНИЦИали-
зируются значением 1. Вообще, каждое из этих двух полей устанавливается
в 1, только если соответствующий файловый объект процесса все еще открыт
процессом; поле сбрасывается в 0, если соответствующий файловый объект
был освобожден, поскольку к нему больше не обращается ни один процесс.
Ответвление нового процесса не увеличивает значения полей readers и
writers, так что они никогда не превышают единицы2. Впрочем, ветвление
увеличивает значения счетчиков обращений всех файловых объектов, кото-
которыми все еще пользуется процесс-родитель (см. главу 3). Таким образом, эти
объекты не освобождаются даже после уничтожения процесса-родителя и
остаются открытыми для его потомков.
Когда процесс делает системный вызов close () для файлового дескриптора,
ассоциированного с каналом, ядро вызывает функцию fput () для соответст-
соответствующего файлового объекта, которая уменьшает счетчик обращений. Если
счетчик становится равным 0, функция вызывает метод release из числа фай-
файловых операций (см. разд. "Системный вызов closeQ " и "Файлы, связанные с
процессом" главы 12).
В зависимости от того, ассоциирован ли файл с каналом чтения или записи,
метод release реализуется либо функцией pipe_read_release (), либо функци-
ей pipe_write_release(). Обе функции вызывают функцию piperelease (),
которая сбрасывает в ноль либо поле readers, либо поле writers структуры
pipeinodeinfo. Функция проверяет, равны ли нулю оба поля, как readers,
так и writers. Если это так, она вызывает метод release для всех буферов ка-
канала и, тем самым, возвращает все страничные кадры канала buddy-системе.
Кроме того, функция освобождает кэшированный страничный кадр, на кото-
который указывает ПОЛе tmpjpage. В прОТИВНОМ случае, еСЛИ либо ПОЛе readers,
либо поле writers нулю не равно, функция возобновляет выполнение процес-
процессов, "спящих" в очереди ожидания, чтобы они смогли распознать изменение
состояния канала.
Чтение из канала
Процесс, которому нужно получить данные из канала, делает системный вы-
вызов reado, передавая ему файловый дескриптор, ассоциированный с тем
концом канала, который предназначен для чтения. Как было сказано в
разд. "Системные вызовы readQ и writeQ " главы 12, ядро, в конечном счете,
вызовет метод read из таблицы файловых операций, ассоциированной с соот-
соответствующим файловым объектом. В случае канала для метода read в табли-
таблице readpipef ops указывает на функцию pipe_read ().
Функция piperead () довольно сложна, поскольку стандарт POSIX определя-
определяет ряд требований для операций чтения канала. В табл. 19.3 суммируется
ожидаемое поведение системного вызова reado, запросившего п байтов из
2 Как мы увидим далее, поля readers и writers работают как счетчики, а не как флаги, когда
они ассоциированы с FIFO-файлами.
канала, размера которого (количество еще не прочитанных байтов в буферах
канала) равен/?.
Системный вызов может заблокировать текущий процесс в двух случаях:
□ буфер канала пуст перед началом системного вызова;
□ буфер канала содержит меньше байтов, чем запрошено, а пишущий про-
процесс приостановлен в ожидании места в буфере.
Обратите внимание, что операция чтения может быть "неблокирующей".
В этом случае она завершается сразу после того, как все доступные байты
(даже если их количество равно нулю) будут скопированы в адресное про-
пространство пользователя3.
Обратите также внимание на то, что значение 0 возвращается системным вы-
вызовом read (), только если канал пуст, и никакой процесс в данный момент не
обращается к файловому объекту, ассоциированному с каналом записи.
Таблица 19.3. Чтение п байтов из канала
Хотя бы один пишущий процесс
1 Нет
Размер Блокирующее чтение пштпниу
канала о 1 Неблокирующее пишущих
н Пишущий Нет "спящих" чтение процессов
процесс "спит" пишущих процессов
Скопировать п
если буфер их размер
пуст
0 < < Скопировать р байтов и возвратить р: в буфере канала
^ останется 0 байтов
р > п Скопировать п байтов и возвратить п: в буфере канала останется р-п байтов
Функция выполняет следующие действия:
1. Получает семафор isem индексного дескриптора.
2. Определяет, равен ли нулю размер канала, для чего читает поле nrbufs
структуры pipeinodeinfo. Если поле равно нулю, значит, все буферы ка-
канала пусты. От этого зависит, должна ли функция возвратить управление,
3 Не задерживающие операции обычно определяются с помощью флага O_NONBLOCK в системном
вызове open (). Такой подход не годится для каналов, поскольку их нельзя открыть. Процесс может
запросить не задерживающую операцию на канале, сделав системный вызов f cntl () для соответ-
соответствующего файлового дескриптора.
или процесс должен быть заблокирован, пока другой процесс запишет
данные в канал (см. табл. 19.3). Тип операции ввода/вывода (блокирующая
или не блокирующая) указан флагом ononblock в поле f_f lags файлового
объекта. Если текущий процесс должен быть блокирован, функция выпол-
выполняет следующие действия:
• ВЫЗЫВаеТ фуНКЦИЮ prepare_to_wait (), Чтобы Добавить Процесс current
в очередь ожидания данного канала (поле wait структуры pipe_
inode_info);
• освобождает семафор индексного дескриптора;
• ВЫЗЫВаеТ фуНКЦИЮ schedule ();
• по окончании ожидания вызывает функцию finishwait о, чтобы уда-
удалить процесс current из очереди ожидания, затем снова получает
семафор is em индексного дескриптора и, наконец, возвращается к
шагу 2.
3. Получает индекс текущего буфера канала из поля curbuf структуры
pipe_inode_info.
4. Выполняет метод тар буфера канала.
5. Копирует запрошенное количество байтов (или столько байтов, сколько
доступно в буфере канала, если их там меньше) из буфера канала в ад-
адресное пространство пользователя.
6. Выполняет метод unmap буфера канала.
7. Обновляет поля offset и len соответствующего объекта pipebuf f ег.
8. Если буфер канала стал пустым (поле len объекта pipebuffer теперь
равно нулю), функция вызывает метод release буфера канала, чтобы ос-
освободить соответствующий страничный кадр, записывает null в поле ops
объекта pipebuf fer, продвигает вперед указатель на текущий буфер ка-
канала, хранящийся в поле curbuf структуры pipeinodeinf о, и уменьшает
счетчик непустых буферов канала в поле nrbuf s.
9. Если все запрошенные байты скопированы, переходит к шагу 12.
10. Если функция находится на этом шаге, значит, не все запрошенные байты
были скопированы в адресное пространство режима пользователя. Если
размер канала больше к^ля (поле nrbufs структуры pipeinodeinfo не
равно нулю), функция возвращается к шагу 3.
11. В буферах канала больше не осталось байтов. Если хотя бы один пишу-
пишущий процесс находится в состоянии ожидания (то есть поле waiting_
writers структуры pipeinodeinf о больше 0), а операция чтения является
блокирующей, функция ВЫЗЫВаеТ фуНКЦИЮ wake_up_interruptible_
sync о, чтобы возобновить выполнение всех пишущих процессов, нахо-
находящихся в очереди ожидания канала, и возвращается к шагу 2.
12. Освобождает семафор isem индексного дескриптора.
13. Вызывает функцию wakeupinterruptibiesyncO, чтобы возобновить
выполнение всех пишущих процессов, находящихся в очереди ожидания
канала.
14. Возвращает количество байтов, скопированных в адресное пространство
пользователя.
Запись в канал
Процесс, которому нужно записать данные в канал, делает системный вызов
write (), передавая ему файловый дескриптор, ассоциированный с тем концом
канала, который предназначен для записи. Ядро удовлетворяет этот запрос,
вызывая метод write соответствующего файлового объекта. В случае с кана-
каналом запись в таблице writepipefops указывает на функцию pipewrite ().
В табл. 19.4 дано резюме поведения определяемого стандартом POSIX для
системного вызова write (), сделавшего запрос на запись п байтов в канал,
имеющий и свободных байтов в буфере. В частности, стандарт требует, что-
чтобы операции записи, в которых участвует небольшое количество байтов, вы-
выполнялись атомарно. Точнее говоря, если два или более процессов парал-
параллельно ведут запись в канал, каждая операция записи, в которую вовлечено
меньше, чем 4096 байтов (размер буфера канала), должна закончиться без
чередования с операциями записи от других процессов в тот же канал. В то
же время операции записи более, чем 4096 байтов, могут быть неатомарны-
неатомарными. Кроме того, они могут перевести вызвавший процесс в состояние ожида-
ожидания.
Таблица 19.4. Запись п байтов в канал
Доступное Хотя бы один читающий процесс Нет читающего
место в 1
буфере и Блокирующая запись Неблокирующая запись "^ч^^а
u<n<4096 Подождать, пока осво- Возвратить Отправить сигнал
бодится n-u байтов, -eagain sigpipe и возвратить
скопировать п байтов -epipe
и возвратить п
/7>4096 Скопировать п байтов Если и>0, скопировать
(переходя в состояние и байтов и возвратить и\
ожидания, когда это иначе возвратить
необходимо) и возвра- -eagain
тить п
и>п Скопировать п байтов
и возвратить п
Кроме того, каждая операция записи в канал должна завершаться неуспешно,
если у канала нет читающего процесса (то есть если поле readers объекта
"индексный дескриптор" канала содержит 0). В этом случае ядро отправляет
сигнал sigpipe пишущему процессу и завершает системный вызов write о с
кодом ошибки -EPIPE, что обычно приводит к хорошо знакомому сообщению
"Broken pipe" ("Канал нарушен").
Функция pipewrite () выполняет следующие действия:
1. Получает семафор isem индексного дескриптора.
2. Проверяет, есть ли у канала хотя бы один читающий процесс. Если нет,
отправляет сигнал sigpipe процессу current, освобождает семафор ин-
индексного дескриптора и возвращает код -epipe.
3. Определяет индекс буфера канала, в который велась запись последний раз,
для чего складывает значения полей curbuf и nrbufs структуры
pipeinodeinf о и из суммы вычитает 1. Если в этом буфере канала доста-
достаточно места для хранения всех байтов, подлежащих записи, функция ко-
копирует в него данные:
• выполняет метод тар буфера канала;
• копирует все байты в буфер канала;
• выполняет метод unmap буфера канала;
• обновляет поле len соответствующего объекта pipebuf f ег;
• переходит к шагу 11.
4. Если поле nrbufs структуры pipeinodeinfo равно 16, значит, нет пустого
буфера канала для данных, которые (еще) должны быть записаны в канал.
В таком случае:
• если операция записи является не блокирующей, функция переходит
к шагу 11, чтобы завершить свое выполнение, возвратив код ошибки
-eagain;
• если операция записи является блокирующей, функция прибавляет 1 к
значению waiting_writers структуры pipeinodeinfo, вызывает функ-
цию preparetowait(), чтобы добавить процесс current в очередь
ожидания данного канала (поле wait структуры pipeinodeinfo), осво-
освобождает семафор индексного дескриптора и вызывает функцию
schedule (). По ОКОНЧаНИИ ОЖИДаНИЯ ВЫЗЫВаеТ фуНКЦИЮ finish_wait(),
чтобы удалить процесс current из очереди ожидания, снова получает
семафор индексного дескриптора, уменьшает значение в поле
waitingwriters и, наконец, переходит к шагу 4.
5. На этом шаге имеется хотя бы один пустой буфер канала. Функция опре-
определяет индекс первого пустого буфера канала, для чего складывает зна-
значения ПОЛеЙ curbuf И nrbuf s Структуры pipe_inode_infо.
6. Выделяет новый страничный кадр в buddy-системе, если поле tmppage
Структуры pipe_inode_inf о равно NULL.
7. Копирует 4096 байтов из адресного пространства режима пользователя
в страничный кадр (временно отображая его в пространство линейных
адресов режима ядра, если это необходимо).
8. Обновляет поля объекта pipebuf fer, ассоциированного с буфером кана-
канала. В частности, записывает в поле page адрес дескриптора страничного
кадра, в поле ops — адрес таблицы anon_pipe_buf_ops, в поле offset —
ноль, а в поле len — количество записанных байтов.
9. Увеличивает счетчик непустых буферов канала, хранящийся в поле
nrbuf s структуры pipe_inode_inf о.
10. Если были записаны не все байты, возвращается к шагу 4.
11. Освобождает семафор индексного дескриптора.
12. Возобновляет выполнение всех читающих процессов из очереди ожида-
ожидания канала.
13. Возвращает количество байтов, записанных в буфер канала (или код
ошибки, если сделать запись было невозможно).
FIFO-файлы
Хотя каналы являются простым, гибким и эффективным механизмом взаимо-
взаимодействия, у них есть один большой недостаток, а именно невозможность от-
открыть уже существующий канал. В результате два произвольных процесса не
могут совместно использовать один канал, если он не был создан их общим
предком.
Этот недостаток является существенным для многих программ. Рассмотрим в
качестве примера сервер базы данных, который непрерывно опрашивает кли-
клиентские процессы, желающие выдать запросы, и отправляет им результаты
поиска в базе данных. Каждый акт взаимодействия между сервером и кон-
конкретным клиентом мог бы происходить с использованием канала. Однако
клиентские процессы обычно создаются командной оболочкой, когда пользо-
пользователь делает явный запрос к базе данных. Таким образом, сервер и клиент-
клиентские процессы не могут пользоваться одним каналом.
Чтобы преодолеть такие ограничения, в Unix-системах был введен специаль-
специальный тип файла, названный именованным каналом, или FIFO-файлом. FIFO
является сокращением для "first in, first out" (первым пришел— первым
ушел), потому что первый байт, записанный в этот специальный файл, будет
прочитан первым. Каждый FIFO-файл во многом аналогичен каналу: он не
имеет блоков на диске в файловой системе и после своего открытия ассоции-
ассоциируется с буфером ядра, временно хранящим данные, которыми обмениваются
два или более процессов.
Благодаря индексному дескриптору на диске к FIFO-файлу может обратиться
любой процесс, потому что имя такого файла находится в дереве каталогов
системы. В приведенном примере взаимодействие между сервером и клиен-
клиентами может быть легко установлено с помощью FIFO-файлов. При запуске
сервер создает FIFO-файл, которым клиентские программы пользуются для
своих запросов. Перед установкой связи каждая клиентская программа созда-
создает еще один FIFO-файл, в который сервер может записать ответ на запрос, и
включает имя этого файла в первый запрос, отправляемый серверу.
В Linux 2.6 FIFO-файлы и каналы почти идентичны и пользуются одними и
теми же структурами pipe_inode_info. Более ТОГО, методы read И write
у FIFO-файлов реализованы теми же piperead () и pipewrite. Фактически,
у FIFO-файлов есть только два отличия:
□ индексные дескрипторы FIFO-файлов существуют в системном дереве
каталогов, а не в специальной файловой системе pipefs;
□ FIFO-файлы являются двунаправленными каналами связи. Иными слова-
словами, можно открыть FIFO-файл в режиме чтения/записи.
Чтобы завершить описание FIFO-файлов, нам остается лишь рассказать, как
они создаются и открываются.
Создание и открытие FIFO-файлов
Процесс создает FIFO-файл с помощью системного вызова mknodo4 (см.
разд. "Файлы устройств" главы 13), передавая ему в качестве параметров
путь к новому FIFO-файлу и значение sififo (Oxioooo), логически сложен-
сложенное (с помощью логической операции ИЛИ) с битовой маской прав доступа
для нового файла. В стандарте POSIX специально для создания FIFO-файла
определена функция mkfifoo. В системе Linux, как и в System V Release 4,
эта функция, включенная в библиотеку С, делает системный вызов mknod ().
После создания FIFO-файла к нему можно обращаться, делая обычные сис-
системные вызовы open о, read о, write о и close о, но виртуальная файловая
4 На самом деле, системный вызов mknod () может быть использован для создания файлов почти
любого типа: файлов блочных и символьных устройств, FIFO-файлов и даже обычных файлов (фай-
(файлы каталогов и сокетов этот вызов создать не может).
система будет обрабатывать их особым образом, поскольку индексный деск-
дескриптор и операции FIFO-файла являются специализированными и не зависят
от файловой системы, в которой находится FIFO-файл.
Стандарт POSIX определяет поведение системного вызова open () для FIFO-
файлов. Оно, в основном, зависит от запрашиваемого типа доступа к файлу,
типа операции ввода/вывода (блокирующая или нет) и наличия других про-
процессов, обращающихся к FIFO-файлу.
Процесс может открыть FIFO-файл для чтения, для записи или для чтения и
записи. Файловые операции, связанные с соответствующим файловым объек-
объектом, настроены на специальные методы в каждом из этих трех случаев.
Когда процесс открывает FIFO-файл, виртуальная файловая система выпол-
выполняет те же действия, что при открытии файла устройства (см. разд. "Работа с
файлами устройств в VFS" главы 13). Объект "индексный дескриптор", ассо-
ассоциированный с открытым FIFO-файлом, инициализируется специфичным для
файловой системы методом суперблока readinode. Этот метод всегда прове-
проверяет, представляет ли индексный дескриптор на диске какой-нибудь специ-
специальный файл, и вызывает функцию initspeciaiinode (), если необходимо.
Со своей стороны, эта функция записывает в поле if op объекта "индексный
дескриптор" адрес таблицы deff ifofops. Впоследствии ядро будет считать
таблицу deffifofops таблицей файловых операций файлового объекта
и выполнит метод open из этой таблицы. Метод реализован функцией
fifo_open().
Функция fifoopeno инициализирует структуры данных, специфичные для
FIFO-файла. В частности, она выполняет следующие действия:
1. Получает семафор isem индексного дескриптора.
2. Проверяет поле ipipe объекта "индексный дескриптор". Если оно содер-
содержит NULL, Выделяет И Инициализирует НОВуЮ Структуру pipe_inode_info?
как описано в разд. "Создание и уничтожение канала"ранее в этой главе.
3. В зависимости от режима доступа, переданного в виде параметра систем-
системному вызову open (), функция инициализирует поле f_ор файлового объек-
объекта адресом соответствующей таблицы файловых операций (табл. 19.5).
Таблица 19.5. Операции FIFO-файла
Режим доступа *™™*»*е Мет°Дread Мет°Д "»**
оперэции
Только чтение read_f if o_fops pipe_read () bad_pipe_w ()
Только запись write_fifo_fops bad_pipe_r() pipe_write()
Чтение/запись rdwr_f if o_f ops pipe_read () pipe_write ()
4. Если в качестве режима доступа указано "только чтение" или "чте-
"чтение/запись", функция увеличивает на единицу значения полей readers и
rcounter структуры pipeinodeinfo. Кроме того, если режимом доступа
является "только чтение", а других читающих процессов нет, функция во-
возобновляет выполнение пишущих процессов из очереди ожидания.
5. Если в качестве режима доступа указано "только запись" или "чте-
"чтение/запись", функция увеличивает на единицу значения полей writers и
wcounter структуры pipeinodeinfo. Кроме того, если режимом доступа
является "только запись", а других пишущих процессов нет, функция во-
возобновляет выполнение читающих процессов из очереди ожидания.
6. Если нет ни пишущих, ни читающих процессов, функция решает, должна
ли она перейти в состояние ожидания или закончить работу, возвратив код
ошибки (табл. 19.6).
Таблица 19.6. Поведение функции fifo_open ()
Режим доступа Блокирующая операция Неблокирующая операция
Только чтение, Успешно завершить работу Успешно завершить работу
есть пишущие процессы
Только чтение, Ждать пишущий процесс Успешно завершить работу
нет пишущих процессов
Только запись, есть Успешно завершить работу Успешно завершить работу
читающие процессы
Только запись, нет Ждать читающий процесс Возвратить -ENXIO
читающих процессов
Чтение/запись Успешно завершить работу Успешно завершить работу
7. Освобождает семафор индексного дескриптора и завершает работу, воз-
возвращая 0 (успех).
Три специализированных таблицы операций FIFO-файла различаются, в ос-
основном, в реализации методов read и write. Если режим доступа допускает
операцию чтения, метод read реализован функцией piperead (). В противном
случае он реализован функцией badpiper (), которая просто возвращает код
ошибки. Аналогично, если режим доступа допускает запись, метод write реа-
реализуется функцией pipewrite(), а в противном случае — функцией
badpipew (), возвращающей код ошибки.
Схема межпроцессного взаимодействия
System VIPC
IPC — сокращение от Interprocess Communication (Межпроцессное взаимо-
взаимодействие). Обычно этот термин относится к комплексу механизмов, позво-
позволяющих процессу, работающему в режиме пользователя:
□ синхронизировать свою работу с другими процессами с помощью сема-
семафоров;
□ отправлять сообщения другим процессам или принимать сообщения от
них;
□ использовать область памяти совместно с другими процессами.
Схема System V IPC впервые появилась в рабочем варианте Unix, носившем
имя "Columbus Unix", и впоследствии была принята в System III фирмы
AT&T. Сейчас она имеется в большинстве Unix-систем, включая Linux.
Структуры данных IPC создаются динамически, когда процесс запрашивает
ресурс IPC (семафор, очередь сообщений или совместно используемую об-
область памяти). Ресурс IPC является постоянным: если процесс не удаляет его
явным образом, он остается в памяти и доступен, пока система не будет вы-
выключена. Ресурс IPC может быть использован любым процессом, а не только
потомками процесса, создавшего ресурс.
Поскольку процесс может затребовать несколько однотипных ресурсов IPC,
каждый новый ресурс идентифицируется 32-битовым ключом IPC, который
аналогичен пути к файлу в дереве каталогов системы. У каждого ресурса IPC
есть также 32-битовый идентификатор IPC, который в определенной степе-
степени аналогичен дескриптору файла, ассоциированному с открытым файлом.
Идентификаторы IPC назначаются ресурсам IPC ядром и являются уникаль-
уникальными в системе, в то время как ключи IPC произвольно выбираются про-
программистами.
Когда несколько процессов хотят взаимодействовать через ресурс IPC, они
пользуются его 1РС-идентификатором.
Работа с ресурсом IPC
Ресурсы IPC создаются С ПОМОЩЬЮ функций semgetO, msggetO И shmgetO,
в зависимости от того, является ли новый ресурс семафором, очередью сооб-
сообщений или совместно используемой областью памяти.
Основная задача каждой из этих функций в том, чтобы по ключу IPC (пере-
(переданному в качестве первого параметра) вычислить идентификатор IPC, кото-
который впоследствии будет использован процессом для доступа к ресурсу. Если
с заданным ключом IPC не ассоциирован ни один ресурс IPC, создается но-
новый ресурс. Если все пройдет гладко, функция возвратит положительный
идентификатор IPC; в противном случае она возвратит один из кодов ошиб-
ошибки, перечисленных в табл. 19.7.
Таблица 19.7. Коды ошибок, возвращаемые при запросе на идентификатор IPC
Код ошибки Описание
eaccess У процесса нет нужных прав доступа
eexist Процесс пытался создать ресурс IPC с уже существующим ключом
EINVAL Функции semget (), msgget () или shmget () передан недопустимый
аргумент
enoent Не существует ресурса IPC с заданным ключом, а процесс не просил
создать новый ресурс
enomem Нет свободной памяти для новых ресурсов IPC
enospc Исчерпан лимит на количество ресурсов IPC
Предположим, два независимых процесса хотят совместно использовать один
ресурс IPC. Этого можно достичь двумя способами:
□ процессы соглашаются на некоторый фиксированный заранее определен-
определенный ключ IPC. Это простейший случай, и он вполне годится для любого
сложного приложения, реализованного несколькими процессами. Однако
есть вероятность, что тот же ключ IPC выберет другая программа. В таком
случае она будет вызывать функции IPC, которые будут возвращать иден-
идентификатор IPC не того ресурса5;
□ ОДИН Процесс вызывает функцию semget (), msgget () ИЛИ shmget (), задав
ipcprivate в качестве ключа IPC. Выделяется новый ресурс IPC, и про-
процесс может либо сообщить его IPC-идентификатор другим процессам в
приложении6, либо самостоятельно породить другой процесс. Этот способ
гарантирует, что ресурс IPC не будет случайно использован другими при-
приложениями.
Последний параметр функций semget (), msgget () И shmget () МОЖет ВКЛЮЧатЬ
в себя три флага. Флаг ipccreat показывает, что ресурс IPC должен быть
5 Функция ft ok () пытается создать новый ключ на основе пути к файлу и 8-битового идентифи-
идентификатора проекта, которые принимает в качестве параметров. Впрочем, она не гарантирует уникально-
уникальности ключа, так как есть небольшая вероятность, что она возвратит одинаковые ключи IPC двум раз-
разным приложениям с разными путями и идентификаторами проектов.
6 Конечно, здесь подразумевается наличие другого способа связи между процессами, не основанно-
основанного на IPC.
создан, если он не существует; флаг ipcexcl требует, чтобы функция завер-
завершилась неуспешно, если ресурс уже существует, а флаг ipccreat установ-
установлен; флаг ipcnowait показывает, что процесс нельзя блокировать, когда он
обращается к ресурсу IPC (как правило, при извлечении сообщения из очере-
очереди или захвате семафора).
Даже если процесс воспользуется флагами ipccreat и ipcexcl, нет способа
гарантировать исключительный доступ к ресурсу IPC, потому что другие
процессы всегда смогут обратиться к тому же ресурсу, используя его IPC-
идентификатор.
Чтобы минимизировать риск обращения не к тому ресурсу, ядро не использу-
использует заново идентификаторы IPC после того, как они освобождаются. Иденти-
Идентификатор IPC, присвоенный ресурсу, почти всегда больше идентификатора,
присвоенного предыдущему ресурсу того же типа. (Единственное исключе-
исключение имеет место при переполнении 32-битового идентификатора IPC.) Каж-
Каждый идентификатор IPC вычисляется на основании порядкового номера об-
обращения к слоту, связанному с типом ресурса, произвольного индекса слота
выделенного ресурса и произвольного значения, превышающего максималь-
максимальное количество выделяемых ресурсов и выбираемого ядром. Пусть s — по-
порядковый номер обращения к слоту, М— максимальное количество ресур-
ресурсов, которые могут быть выделены, а / — индекс слота, причем 0< /<М, тогда
идентификатор ресурса IPC вычисляется по следующей формуле:
Идентификатор IPC = s x M+ i
В Linux 2.6 значение М равно 32 768 (макрос ipcmni). Порядковый номер об-
обращения к слоту s инициализируется нулем и увеличивается на 1 при каждом
выделении ресурса. Когда s достигнет определенного порога, зависящего от
типа ресурса IPC, его значение обнуляется.
Ресурсы IPC каждого типа: семафоры, очереди сообщений и совместно ис-
используемые области памяти) имеют собственные структуры ipcids с поля-
полями, перечисленными в табл. 19.8.
Таблица 19.8. Поля структуры ±pc_ids
Тип Поле Описание
int inuse Количество выделенных ресурсов IPC
int max_id Максимальный используемый индекс слота
unsigned short seq Порядковый номер обращения к слоту для
следующего выделения
unsigned short seqjnax Максимальный порядковый номер обращения
к слоту
Таблица 19.8 (окончание)
Тип Поле Описание
struct semaphore sem Семафор, защищающий структуру ipc_ids
struct ipc_id_ary nullentry Структура-"пустышка", на которую указывает поле
entries, если данный ресурс IPC не может быть
проинициализирован (в нормальной ситуации не
используется)
struct ipc_id_ary * entries Указатель на структуру ipc_id_ary данного
ресурса
Структура ipcidary состоит из двух полей: р и size. Поле р является масси-
массивом указателей на структуры kernipcperm, по одной на каждый выделяемый
ресурс. Поле size — размер этого массива. Изначально массив содержит 1,16
или 128 указателей соответственно для совместно используемых областей
памяти, очередей сообщений и семафоров. Ядро динамически увеличивает
размер массива, когда в этом возникает необходимость. Однако для количе-
количества ресурсов каждого типа существует верхняя граница. Системный адми-
администратор может поднять эти границы, отредактировав файлы /proc/sys
/kernel/sem, /proc/sys/kernel/msgmni и /proc/sys/kernel/shmmni соответственно.
Каждая структура данных kernipcperm связана с ресурсом IPC и содержит
поля, перечисленные в табл. 19.9. Поля uid, gid, cuid и cgid содержат, соот-
соответственно, идентификаторы пользователя и группы создателя, а также иден-
идентификаторы пользователя и группы текущего владельца ресурса. Битовая
маска mode включает в себя шесть флагов, которые определяют права на
чтение и запись для владельца ресурса, группы ресурса и всех прочих
пользователей. Права доступа к IPC-ресурсу аналогичны правам доступа
к обычным файлам, описанным в главе /), с тем исключением, что право на
выполнение игнорируется.
Таблица 19.9. Поля структуры kern_±pcjperm
Тип Поле Описание
spinlockt lock Спин-блокировка для защиты дескриптора ресурса
IPC
int deleted Флаг, устанавливаемый, если ресурс был освобожден
int key Ключ IPC
unsigned int uid Идентификатор пользователя-владельца
unsigned int gid Идентификатор группы-владельца
unsigned int cuid Идентификатор пользователя-создателя
Таблица 19.9 (окончание)
Тип Поле Описание
unsigned int cgid Идентификатор группы-создателя
unsigned short mode Битовая маска прав доступа
unsigned long seq Порядковый номер обращения к слоту
void * security Указатель на структуру безопасности (используется
в SELinux)
Структура kernipcperm также имеет поле key (которое содержит IPC-
ключ соответствующего ресурса) и поле seq (которое содержит порядковый
номер обращения к слоту, необходимый для вычисления 1РС-идентификатора
ресурса).
ФуНКЦИИ semctl (), msgctl () И shmctl () МОГуТ быть ИСПОЛЬЗОВаНЫ ДЛЯ работы
с ресурсами IPC. Подкоманда ipcset позволяет процессу изменить иденти-
идентификаторы владельца (пользователя и группы), а также битовую маску прав
доступа в структуре ipcperm. Подкоманды ipcstat и ipcinfo позволяют
получить некоторую информацию о ресурсе. Наконец, подкоманда ipcrmid
освобождает ресурс IPC. Доступны и другие специализированные подкоман-
подкоманды, в зависимости от типа ресурса7.
Когда ресурс IPC создан, процесс может работать с ним, пользуясь несколь-
несколькими специализированными функциями. Процесс может получать и освобо-
освобождать семафор IPC с помощью функции semop (). Когда процессу нужно от-
отправить или получить сообщение IPC, он вызывает соответственно функцию
msgsndo и msgrcv(). Наконец, процесс может присоединять и отсоединять
совместно используемую область памяти IPC в своем адресном пространстве,
обращаясь к функциям shmat () и shmdt () соответственно.
Системный вызов ipc()
Все функции межпроцессного взаимодействия должны быть реализованы
соответствующими системными вызовами Linux. Фактически в архитектуре
80x86 имеется только один системный вызов такого взаимодействия — ipc ().
Когда процесс вызывает какую-нибудь IPC-функцию, скажем, msggeto, он,
в действительности, вызывает интерфейсную функцию из библиотеки С. Эта
функция, в свою очередь, делает системный вызов ipc (), передавая ему все
7 Существует конструктивный недостаток IPC, заключающийся в том, что процесс режима пользо-
пользователя не может атомарным образом создать и инициализировать семафор IPC, потому что эти две
операции выполняются двумя разными функциями IPC.
параметры функции msgget () плюс код соответствующей подкоманды, в дан-
данном случае, msgget. Служебная процедура sysipc () проверяет код подкоман-
подкоманды и вызывает функцию ядра, реализующую запрошенный сервис.
"Мультиплексный" системный вызов ipc () является наследием прежних вер-
версий Linux, в которых код межпроцессного взаимодействия был включен в
динамически подгружаемый модуль (см. приложение 2). Тогда не было
смысла резервировать в таблице systemcaii место под несколько системных
вызовов для компонента, которого может и не быть в системе, и поэтому раз-
разработчики приняли мультиплексный подход.
В наше время уже нельзя скомпилировать System V IPC как модуль, и боль-
больше нет оправдания применению единственного системного вызова для меж-
межпроцессного взаимодействия. Фактически, в архитектуре Alpha фирмы
Hewlett-Packard и в IA-64 фирмы Intel операционная система Linux предос-
предоставляет отдельный системный вызов для каждой 1РС-функции.
Семафоры IPC
Семафоры межпроцессного взаимодействия аналогичны семафорам ядра,
описанным в главе 5. Это счетчики, позволяющие обеспечить контролируе-
контролируемый доступ нескольких процессов к совместно используемым структурам
данных.
Значение семафора положительно, если защищаемый ресурс доступен, и рав-
равно 0, когда он временно недоступен. Процесс, желающий обратиться к ресур-
ресурсу, пытается уменьшить значение семафора, а ядро блокирует процесс, пока
операция с семафором не даст положительный результат. Когда процесс ос-
освобождает защищенный ресурс, он увеличивает значение семафора, в резуль-
результате чего "пробуждается" процесс, ожидающий семафор.
На практике работать с IPC-семафорами сложнее, чем с семафорами ядра, по
двум основным причинам:
□ каждый семафор IPC представляет собой набор из одного или нескольких
семафорных значений, а не просто одно значение, как семафор ядра. Это
означает, что один ресурс IPC может защищать несколько независимых
совместно используемых структур. Количество семафорных значений в
каждом семафоре IPC должно быть задано параметром функции semget ()
при выделении ресурса. Далее в этой книге мы будем называть счетчики
внутри семафора IPC примитивными семафорами. Существуют ограниче-
ограничения как на количество семафоров IPC (по умолчанию 128), так и на коли-
количество примитивных семафоров внутри одного семафора IPC (по умолча-
умолчанию 250). Впрочем, системный администратор может без труда изменить
их, отредактировав файл /sys/kernel/sem;
□ семафоры System V IPC обеспечивают защищенный от сбоев механизм
для ситуаций, в которых процесс прерывается, будучи не в состоянии от-
отменить операции над семафором, запущенные ранее. Если процесс ис-
использует этот механизм, такие операции называются отменяемыми сема-
семафорными операциями. Когда процесс "умирает", все его семафоры IPC
могут восстановить значения, которые были бы у них, если бы процесс во-
вовсе не запускал операции над семафором. Это убережет другие процессы,
пользующиеся теми же семафорами, от неопределенно долгого ожидания
из-за того, что оборвавшийся процесс не смог явно отменить семафорные
операции.
Вначале мы вкратце опишем типичные действия, выполняемые процессом,
желающим обратиться к одному или нескольким ресурсам, защищенным се-
семафором IPC:
1. Вызывает интерфейсную функцию semget (), чтобы получить идентифика-
идентификатор семафора IPC, передавая ей в качестве параметра ключ семафора IPC,
защищающего совместно используемые ресурсы. Если процесс желает
создать новый IPC-семафор, он задает флаг ipccreate или ipcprivate и
количество нужных ему семафоров (см. разд. "Работа с ресурсом IPC"
ранее в этой главе).
2. Вызывает интерфейсную функцию semop (), чтобы проверить и уменьшить
значения всех примитивных семафоров, имеющих отношение к ситуации.
Если все проверки пройдут успешно, значения уменьшаются, функция
возвращает управление, а процесс получает разрешение обратиться к за-
защищенным ресурсам. Если некоторые семафоры в данный момент заняты,
процесс обычно приостанавливается, пока ресурсы не будут освобождены
каким-нибудь другим процессом. Функция принимает в качестве парамет-
параметров IPC-идентификатор семафора; массив целых чисел, задающих опера-
операции, которые следует атомарным образом выполнить над примитивными
семафорами; а также количество этих операций. Дополнительно процесс
может задать флаг semundo, предписывающий ядру отменить операции,
если процесс завершится, не освободив примитивные семафоры.
3. Освобождая защищенные ресурсы, процесс снова вызывает функцию
semop (), чтобы атомарно увеличить значения всех вовлеченных примитив-
примитивных семафоров.
4. В качестве необязательного шага процесс вызывает интерфейсную функ-
функцию semctio, задавая команду ipcrmid, чтобы удалить семафор из сис-
системы.
Теперь мы можем обсудить, как ядро реализует семафоры IPC. Структуры
данных, участвующие в этом, изображены на рис. 19.1. Переменная semids
содержит структуры ipcids типа "семафор IPC". Соответствующая структу-
pa ipcidary содержит массив указателей на структуры semarray, по одному
элементу на каждый ресурс-семафор IPC.
Рис. 19.1. Структуры семафора IPC
Формально этот массив хранит указатели на структуры kernipcperm, но
каждая из них является первым полем структуры semarray. Все поля струк-
структуры semarray перечислены в табл. 19.10.
Таблица 19.10. Поля структуры sem_array
Тип Поле Описание
struct kern_ipc_perm sem_perm Структура kern_ipc_perm
long sem_otime Отметка времени о последней
операции semop ()
long sem_ctime Отметка времени о последнем
изменении
struct sem * sembase Указатель на первую структуру sem
struct sem_queue * sem_pending Ждущие операции
struct sem_queue ** sem_pending_last Последняя ждущая операция
struct sem_undo * undo Запросы на отмену
unsigned long sem_nsems Количество семафоров в массиве
Поле sembase указывает на массив структур sem, по одной на каждый прими-
примитивный семафор. Структура sem состоит из двух полей:
□ semvai — значение семафорного счетчика;
□ sempid — идентификатор последнего процесса, обратившегося к семафору.
Процесс может получить это значение с помощью интерфейсной функции.
Отменяемые семафорные операции
Если процесс внезапно прерывается, он не может отменить операции, кото-
которые начал (например, освободить зарезервированные семафоры). Объявляя
эти операции отменяемыми, процесс разрешает ядру перевести семафоры
в корректное состояние и позволить другим процессам продолжить рабо-
работу. Процессы могут запросить отменяемые операции, задав флаг semundo
В функции semop ().
Информация, помогающая ядру обратить отменяемые операции, выполнен-
выполненные данным процессом над данным семафором IPC, хранится в структуре
semundo. Структура содержит IPC-идентификатор семафора и массив целых
чисел, обозначающих изменения, внесенные в значения примитивных сема-
семафоров всеми отменяемыми операциями, выполненными процессом.
Проиллюстрируем на простом примере, как используются элементы структу-
структуры semundo. Рассмотрим процесс, который работает с семафором IPC, со-
содержащим четыре примитивных семафора. Предположим, он вызывает
функцию semop (), чтобы увеличить первый счетчик на 1 и уменьшить второй
на2. Если он укажет флаг semundo, целое в первом элементе массива в
структуре semundo будет уменьшено на 1, целое во втором элементе— уве-
увеличено на 2, а остальные два целых числа останутся прежними. Последую-
Последующие отменяемые операции над семафором IPC, выполненные тем же процес-
процессом, изменяют значения целых в структуре semundo аналогичным образом.
Когда процесс завершится, любое ненулевое значение в этом массиве будет
свидетельствовать об одной или нескольких несбалансированных операциях
над соответствующим примитивным семафором. Ядро обращает такие опе-
операции простым прибавлением этого ненулевого значения к счетчику семафо-
семафора. Другими словами, изменения, внесенные оборванным процессом, отме-
отменяются, а изменения, сделанные другими процессами, отражают состояние
семафоров.
Для каждого процесса ядро отслеживает все семафорные ресурсы, обрабаты-
обрабатываемые отменяемыми операциями, чтобы иметь возможность "откатить" эти
операции, если процесс неожиданно прервется. Кроме того, ядро должно для
каждого семафора отслеживать структуры semundo, чтобы в состоянии
быстро обратиться к ним, если процесс вызовет функцию semcti () с целью
записать конкретное значение в счетчик примитивного семафора или унич-
уничтожить семафор IPC.
Ядро может эффективно решать эти задачи благодаря двум спискам, которые
мы будем называть списком для процессов и списком для семафоров. Первый
список отслеживает все семафоры, на которые воздействовал данный процесс
с помощью отменяемых операций. Второй список отслеживает все процессы,
действующие на данный семафор отменяемыми операциями. Более кон-
конкретно:
□ список для процессов включает в себя все структуры semundo, соответст-
соответствующие семафорам IPC, над которыми процесс выполнил отменяемые
операции. Поле sysvsem.undo_iist дескриптора процесса указывает на
структуру типа semundoiist, которая, в свою очередь, содержит указа-
указатель на первый элемент списка, причем поле proc next каждой структуры
semundo указывает на следующий элемент. (Как было сказано в главе 3,
процессы-клоны, созданные передачей флага clonesysvsem системному
вызову clone (), пользуются одним общим списком отменяемых операций,
ПОСКОЛЬКУ у НИХ Общий ДеСКрИПТОр semundoiist);
□ список для семафоров включает в себя все структуры semundo, соответст-
соответствующие процессам, выполнившим отменяемые операции над семафором.
Поле undo структуры semarray указывает на первый элемент списка, в то
время как поле idnext каждой структуры semundo указывает на следую-
следующий элемент.
Список для процессов нужен в том случае, когда процесс завершается. Функ-
Функция exitsemO, вызванная функцией doexito, перебирает элементы этого
списка и отменяет результат любой несбалансированной операции для каж-
каждого семафора IPC, затронутого процессом. Список для семафоров, наоборот,
нужен в том случае, когда процесс вызывает функцию semcti (), чтобы явно
записать значение в примитивный семафор. Ядро обнуляет соответствующий
элемент в массивах всех структур semundo, ссылающихся на этот семафор
IPC, потому что больше нет смысла обращать результаты предыдущих отме-
отменяемых операций над этим примитивным семафором. Кроме того, список для
семафоров используется при уничтожении семафора: все задействованные
структуры sem undo делаются недействительными путем записи значений -1
В ИХ ПОЛЯ semid .
Очередь ждущих запросов
Ядро связывает с каждым семафором межпроцессного взаимодействия оче-
очередь ждущих запросов, чтобы идентифицировать процессы, ожидающие
8 Обратите внимание, что они принудительно делаются недействительными, но не освобождаются,
поскольку было бы слишком дорого удалять структуры из всех списков для процессов.
один (или несколько) семафоров в массиве. Очередь представляет собой
двунаправленный список структур semqueue, чьи поля перечислены в
табл. 19.11. На первый и последний запрос в очереди ссылаются, соответст-
соответственно, ПОЛЯ sem_pending И sem_pending_last структуры sem_array. Последнее
поле позволяет обращаться со списком как с FIFO-файлом: новые ждущие
запросы добавляются в конец списка и обслуживаются позже. Самыми важ-
важными полями ждущего запроса являются nsops (в котором хранится количе-
количество примитивных семафоров, вовлеченных в операцию) и sops (которое ука-
указывает на массив целых чисел, описывающих каждую семафорную опера-
операцию). Поле sleeper содержит адрес дескриптора "спящего" процесса,
запросившего операцию.
Таблица 19.11. Поля структуры sem_queue
Тип Поле Описание
struct semqueue * next Указатель на следующий элемент очереди
struct sem_queue ** prev Указатель на предыдущий элемент очереди
struct taskstruct * sleeper Указатель на "спящий" процесс, запросивший
семафорную операцию
struct sem_undo * undo Указатель на структуру sem_undo
int pid Идентификатор процесса
int status Состояние завершения операции
struct semarray * sma Указатель на дескриптор семафора I PC
int id Индекс слота семафора IPC
struct sembuf * sops Указатель на массив ждущих операций
int nsops Количество ждущих операций
int alter Флаг, обозначающий, изменяет ли операция
массив семафоров
На рис. 19.1 изображен семафор, у которого есть три ждущих запроса. Вто-
Второй и третий запросы ссылаются на отменяемые операции, поэтому у каждо-
каждого из них поле undo структуры semqueue указывает на соответствующую
структуру semundo. Что касается первого запроса, его поле undo содержит
null, поскольку соответствующая операция не является отменяемой.
Сообщения IPC
Процессы могут поддерживать связь друг с другом при помощи сообщений
IPC. Каждое сообщение, сгенерированное процессом, поступает в очередь
сообщений IP С, где остается, пока другой процесс не прочитает его.
Сообщение состоит из заголовка фиксированной длины и текста переменной
длины. Оно может быть помечено целочисленным значением (типом сооб-
сообщения), которое позволяет процессу избирательно извлекать сообщения из
очереди9. После того, как какой-нибудь процесс прочитает сообщение из оче-
очереди сообщений IPC, ядро уничтожает это сообщение. Следовательно, только
один процесс может получить данное сообщение.
Чтобы отправить сообщение, процесс вызывает функцию msgsnd (), передавая
ей следующие параметры:
□ IPC-идентификатор очереди сообщений;
□ размер текста сообщения;
□ адрес буфера режима пользователя, в котором хранятся подряд тип сооб-
сообщения и его текст.
Чтобы прочитать сообщение, процесс вызывает функцию msgrcvo, переда-
передавая ей:
□ IPC-идентификатор очереди сообщений;
□ указатель на буфер режима пользователя, в который следует скопировать
тип и текст сообщения;
□ размер этого буфера;
□ значение t, определяющее, какое сообщение должно быть прочитано.
Если t равно 0, функция возвращает первое сообщение из очереди. Если t по-
положительно, функция возвращает первое же сообщение, тип которого равен t.
Наконец, если / отрицательно, она возвращает первое сообщение, чей тип яв-
является наименьшим числом, которое меньше или равно абсолютному значе-
значению t.
Во избежание истощения ресурсов установлены определенные ограничения
на количество очередей сообщений IPC (по умолчанию 16), на размер каждо-
каждого сообщения (по умолчанию 8192 байта) и на максимальный суммарный
объем сообщений в очереди (по умолчанию 16 384 байта). Впрочем, как все-
всегда, системный администратор может отрегулировать эти значения, произво-
производя запись в файлы /proc/sys/kernel/msgmni, /proc/sys/kernel/msgmnb и
/proc/sys/kernel/msgmax соответственно.
Структуры, ассоциированные с очередями сообщений IPC, изображены на
рис. 19.2. Переменная msgids хранит структуру ipcids типа "очередь сооб-
сообщений IPC". Соответствующая структура ipcidary содержит массив указа-
9 Как мы увидим позже, очередь сообщений реализована в виде связного списка. Поскольку сооб-
сообщения могут быть прочитаны в порядке, отличном от "первым пришел — первым ушел", название
"очередь" не вполне удачно. Впрочем, новые сообщения всегда помещаются в конец этого списка.
телей на структуры msgqueue, по одному элементу на каждую очередь сооб-
сообщений IPC. Формально массив хранит указатели на структуры kemipcperm,
но каждая такая структура просто является первым полем структуры
msgqueue. Все поля структуры msgqueue перечислены в табл. 19.12.
Рис. 19.2. Структуры очереди сообщений IPC
Таблица 19.12. Структура msgqueue
Тип Поле Описание
struct kern_ipc_perm q_perm Структура kern_ipc_perm
long q_stime Время последнего вызова функции msgsnd ()
long q_rtime Время последнего вызова функции msgrcv ()
long q_ctime Время последнего изменения
unsigned long q_qcbytes Количество байтов в очереди
unsigned long q_qnum Количество сообщений в очереди
unsigned long q_qbytes Максимальное количество байтов в очереди
int q_lspid Идентификатор процесса последнего вызова
функции msgsnd ()
int qlrpid Идентификатор процесса последнего вызова
функции msgrcv ()
struct list_head qjnessages Список сообщений в очереди
struct list_head ^receivers Список процессов, получающих сообщения
struct list_head q_senders Список процессов, отправляющих сообще-
сообщения
Самым важным полем является qmessages, представляющее голову (то есть
первый элемент-"пустышку|!) двунаправленного циклического списка, со-
содержащего все сообщения, находящиеся в очереди на данный момент.
Каждое сообщение состоит из одной или нескольких страниц, выделяемых
динамически. В начале первой страницы находится заголовок сообщения.
Это структура типа msgmsg, поля которой перечислены в табл. 19.13. Поле
miist содержит указатели на предыдущее и следующее сообщения в очере-
очереди. Текст сообщения начинается сразу за дескриптором msgmsg. Если сооб-
сообщение длиннее 4072 байтов (размер страницы минус размер дескриптора
msgmsg), он продолжается на следующей странице, адрес которой находится
в поле next дескриптора msgmsg. Второй страничный кадр начинается с деск-
дескриптора типа msgmsgseg, содержащего всего лишь указатель next с адресом
необязательной третьей страницы, и т. д.
Таблица 19.13. Структура msg_msg
Тип Поле Описание
struct list_head m_list Указатели, используемые в списке сообщений
long m_type Тип сообщения
int m_ts Размер текста сообщения
struct msg_msgseg * next Следующая часть сообщения
void * security Указатель на структуру безопасности
(используется в SELinux)
Когда очередь сообщений переполнена (достигнуто либо максимальное ко-
количество сообщений, либо максимальный общий размер), процессы, пытаю-
пытающиеся поставить в очередь новые сообщения, могут быть блокированы. Поле
qsenders структуры msgqueue является головой списка, включающего в себя
указатели на дескрипторы всех заблокированных процессов, отправляющих
сообщения.
Каждый принимающий процесс может быть заблокирован, если очередь со-
сообщений пуста (или в очереди отсутствуют сообщения типа, указанного про-
процессом). Поле qreceivers структуры msgqueue является головой списка
структур msgreceiver, по одной на каждый задержанный процесс, прини-
принимающий сообщение. Каждая из этих структур содержит указатель на деск-
дескриптор процесса, указатель на структуру msgmsg сообщения и тип запрошен-
запрошенного сообщения.
Совместно используемая память IPC
Самым эффективным механизмом межпроцессного взаимодействия является
совместно используемая память. Два или более процессов могут обращаться
к общим структурам данных, поместив их в совместно используемую об-
область памяти IPC. Каждый процесс, желающий обратиться к структурам
данных в совместно используемой области памяти IPC, должен добавить в
свое адресное пространство новую область памяти (см. главу 9), которая ото-
отображает страничные кадры, ассоциированные с совместно используемой об-
областью памяти IPC. Затем ядро сможет с легкостью управлять этими стра-
страничными кадрами при помощи механизма выделения страниц "по требо-
требованию".
Как это было с семафорами и очередями, для получения 1РС-идентификатора
совместно используемой области памяти вызывается функция shmget (), кото-
которая может создать ресурс, если он еще не существует.
Функция shmat () вызывается для "присоединения" совместно используемой
области памяти IPC к процессу. В качестве параметра она принимает иден-
идентификатор совместно используемой памяти IPC и пытается добавить соответ-
соответствующую область памяти в адресное пространство вызвавшего процесса.
Этому процессу может потребоваться конкретный начальный линейный ад-
адрес для области памяти, но обычно адрес не важен, и каждый процесс, обра-
обращающийся к совместно используемой области памяти, может пользоваться
каким-то другим адресом в своем адресном пространстве. Функция shmat о
не изменяет Таблицы Страниц процесса. Чуть позже мы опишем, что делает
ядро, когда процесс пытается обратиться к странице, принадлежащей новой
области памяти.
Функция shmdt () вызывается, чтобы "отсоединить" совместно используемую
область памяти IPC, заданную с помощью идентификатора IPC. Иными сло-
словами, функция удаляет соответствующую область памяти из адресного про-
пространства процесса. Вспомним, что такой ресурс, как совместно используе-
используемая память IPC, постоянен: даже если ни один процесс им не пользуется, его
страницы не могут быть утилизированы, хотя и могут быть выгружены на
диск.
Как и для ресурсов IPC других типов, во избежание чрезмерного захвата па-
памяти процессами режима пользователя, установлены ограничения на количе-
количество совместно используемых областей памяти IPC (по умолчанию 4096), на
размер каждого сегмента (по умолчанию 32 Мбайт) и на суммарный объем
всех сегментов (по умолчанию 8 Гбайт). Впрочем, как всегда, системный ад-
администратор может отрегулировать эти значения, производя запись в файлы
/proc/sys/kernel/shmmni, /proc/sys/kernel/shmmax и /proc/sys/kernel/shmall соот-
соответственно.
Рис. 19.3. Структуры совместно используемой памяти IPC
Структуры, связанные с областями совместно используемой памяти IPC, по-
показаны на рис. 19.3. Переменная shmids хранит структуру ipcids типа "со-
"совместно используемая память IPC". Соответствующая структура ipcidary
содержит массив указателей на структуры shmidkernei, по одному элементу
на каждую совместно используемую область памяти IPC. Формально массив
хранит указатели на структуры kemipcperm, но каждая такая структура
просто является первым полем структуры shmidkernei. Все поля структуры
shmidkernei перечислены в табл. 19.14.
Таблица 19.14. Поля структуры shmid_kernel
Тип Поле Описание
struct kern_ipc_perm shm_perm Структура kern_ipc_perm
struct file * shm_file Специальный файл сегмента
int id Индекс слота сегмента
unsigned long shm_nattch Количество присоединенных сегментов
unsigned long shm_segsz Размер сегмента в байтах
long shm_atim Время последнего обращения
Таблица 19.14 (окончание)
Тип Поле Описание
long shm_dtim Время последнего присоединения
long snm_ctim Время последнего изменения
int shmcprid Идентификатор процесса-создателя
int shmlprid Идентификатор последнего обратившегося
процесса
struct user_struct * mlock_user Указатель на дескриптор user_struct поль-
пользователя, заблокировавшего в оперативной
памяти ресурс "совместно используемая
память" (см. главу 3)
Самым важным является поле shm_f iie, хранящее адрес файлового объекта.
Это отражает тесную интеграцию совместно используемой памяти IPC со
слоем виртуальной файловой системы в Linux 2.6. В частности, каждая со-
совместно используемая область памяти IPC ассоциирована с файлом, принад-
принадлежащим специальной файловой системе shm (см. разд. "Специальные фай-
файловые системы" главы 12).
Поскольку у файловой системы shm нет точки монтирования в дереве катало-
каталогов системы, пользователи не могут открывать ее файлы и обращаться к ним
с помощью обычных системных вызовов виртуальной файловой системы.
Однако, когда процесс "присоединяет" сегмент, ядро вызывает функцию
dommap () и создает новое совместно используемое отображение файла в па-
память в адресном пространстве процесса. Таким образом, файлы, принадле-
принадлежащие специальной файловой системе shm, имеют только один метод файло-
файлового Объекта, mmap, КОТОРЫЙ реализуется функцией shm_mmap ().
Как видно из рис. 19.3, область памяти, которая соответствует совместно ис-
используемой области памяти IPC, описывается объектом vmareastruct (см.
разд. "Отображение в память" главы 16). Его поле vmfile указывает на
файловый объект файла в специальной файловой системе, который, в свою
очередь, указывает на объекты "элемент каталога" и "индексный дескрип-
дескриптор". Номер индексного дескриптора, хранящийся в поле iino индексного
дескриптора, фактически является индексом слота для совместно используе-
используемой области памяти IPC, так что индексный дескриптор неявно ссылается на
Дескриптор shmidkernel.
Как это обычно бывает с каждым совместно используемым отображением
файла в память, страничные кадры помещаются в кэш страниц посредством
объекта addressspace, который встроен в индексный дескриптор и на
который указывает поле imapping индексного дескриптора (см. рис. 16.2).
В случае со страничными кадрами, принадлежащими совместно используе-
используемой области памяти IPC, методы объекта addressspace находятся в глобаль-
глобальной переменной shmem_aops.
Выгрузка страниц
совместно используемых областей памяти IPC
Ядро должно быть очень осторожно при выгрузке страниц, принадлежащих
совместно используемым областям памяти, и роль кэша подкачки здесь чрез-
чрезвычайно важна (эта тема уже затрагивалась в разд. "Кэш подкачки" в гла-
главе 17).
Страницы совместно используемой области памяти IPC являются выгружае-
выгружаемыми, но не синхронизируемыми (см. табл. 17.1), потому что они отобража-
отображают специальный индексный дескриптор, не имеющий образа на диске. Сле-
Следовательно, чтобы утилизировать страницу из совместно используемой об-
области памяти IPC, ядро должно записать ее в область подкачки. Поскольку
совместно используемая область памяти IPC является постоянным ресурсом
(то есть ее страницы должны сохраняться, даже если сегмент не присоединен
ни к одному процессу), ядро не может просто выбросить эти страницы, даже
если ими больше не пользуется никакой процесс.
Рассмотрим, как алгоритм PFRA выполняет утилизацию страничного кадра
из совместно используемой области памяти IPC. Все происходит, как описано
в разд. "Утилизация при дефиците памяти" в главе 17, до того момента, как
страницу начнет изучать функция shrinkiisto. Поскольку эта функция не
выполняет никаких специальных проверок для совместно используемых об-
областей памяти IPC, она, в конечном счете, вызывает функцию trytounmap (),
чтобы удалить каждую ссылку на страничный кадр из адресных пространств
режима пользователя. Как было сказано в разд. "Обратное отображение" в
главе 17, соответствующие записи в Таблицах Страниц просто очищаются.
Затем функция shrinkiist о проверяет флаг PGdirty данной страницы и
вызывает функцию pageouto. Поскольку совместно используемые области
памяти IPC помечаются как "грязные" в момент их выделения, функция
pageouto вызывается всегда. В свою очередь, функция pageouto вызывает
метод writepage объекта addressspace отображенного файла.
Функция shmemwritepage (), реализующая метод writepage ДЛЯ страниц ИЗ
совместно используемых областей памяти IPC, выделяет новый страничный
слот в области подкачки и переносит страницу из кэша страниц в кэш под-
подкачки (что сводится к смене объекта addressspace, владеющего страницей).
Функция также сохраняет идентификатор выгруженной страницы в структу-
структуре shmeminodeinfo, которая заключает в себе объект "индексный дескрип-
тор" совместно используемой области памяти IPC, и снова устанавливает у
страницы флаг PGdirty. Как показано на рис. 17.5, функция shrinkiisto
проверяет флаг PGdirty и прерывает процедуру утилизации, оставляя стра-
страницу в активном списке.
Рано или поздно этот страничный кадр снова будет рассмотрен алгоритмом
утилизации. И опять функция shrinkiisto попытается сбросить страницу
на диск, вызвав функцию pageout (). Но на этот раз страница будет находить-
находиться в кэше подкачки, т. е. ею будет владеть объект addressspace подсистемы
ПОДКачкИ, а Именно swapper_space. Соответствующий метод writepage, ТОЧ-
нее, функция swapwritepage (), фактически запускает операцию записи в об-
область подкачки (см. разд. "Выгрузка страниц" главы 17). Когда функция
pageout о возвратит управление, функция shrinkiisto убедится, что стра-
страница теперь не "грязная", удалит ее из кэша подкачки и вернет ее buddy-
системе.
Выделение страниц по требованию
для совместно используемых областей памяти IPC
Страницы, присоединяемые к процессу функцией shmato, являются "пус-
"пустышками". Функция добавляет новую область памяти в адресное пространст-
пространство процесса, но не модифицирует Таблицы Страниц процесса. Кроме того,
как мы убедились, страницы совместно используемой области памяти IPC
могут быть выгружены на диск. Следовательно, работа с этими страницами
ведется с помощью механизма выделения страниц по требованию.
Как мы знаем, исключение "ошибка обращения к странице" возникает, когда
процесс пытается обратиться к ячейке совместно используемой области па-
памяти IPC, чей страничный кадр не был ему присвоен. Обработчик этого ис-
исключения определяет, что проблемный адрес находится в адресном про-
пространстве процесса, а соответствующая запись в Таблице Страниц обнулена.
Тогда обработчик исключения вызывает функцию donopage () (см. главу 9).
Эта функция проверяет, определен ли метод nopage для данной области памя-
памяти. Метод вызывается, и в запись Таблицы Страниц заносится возвращенный
им адрес (см. также разд. "Выделение страниц по требованию для отобра-
отображения в память" главы 16).
В совместно используемых областях памяти IPC метод nopage определен все-
всегда. Он реализован функцией shmemnopage (), которая выполняет следующие
действия:
1. Проходит по цепочке указателей в объектах виртуальной файловой систе-
системы и получает адрес объекта "индексный дескриптор" для 1РС-ресурса
"совместно используемая память" (см. рис. 19.3).
2. Вычисляет логический номер страницы внутри сегмента по значению в
поле vmstart дескриптора области памяти и запрошенному адресу.
3. Проверяет, находится ли страница в кэше страниц. Если это так, завершает
работу, возвращая адрес дескриптора страницы.
4. Проверяет, находится ли страница в кэше подкачки, и не устарела ли она.
Если это так, завершает работу, возвращая адрес дескриптора страницы.
5. Проверяет структуру shmeminodeinfo, включающую в себя индексный
дескриптор, на предмет того, содержит ли она идентификатор выгружен-
выгруженной страницы для логического номера страницы. Если это так, функция
выполняет загрузку выгруженной страницы, вызывая функцию
readswapcacheasync () (см. главу 17), ждет окончания пересылки данных
и завершает работу, возвращая адрес дескриптора страницы.
6. Если функция на этом шаге, страница не находится в области подкачки.
Поэтому функция выделяет новую страницу из buddy-системы, заносит ее
в кэш страниц и возвращает ее адрес.
Функция donopage () модифицирует запись Таблицы Страниц процесса, со-
соответствующую адресу, из-за которого возникло исключение, так, чтобы эта
запись указывала на страничный кадр, возвращенный методом nopage.
Очереди сообщений POSIX
Стандарт POSIX (IEEE Std 1003.1-2001) определяет механизм межпроцессно-
межпроцессного взаимодействия, основанный на очередях сообщений и известный под на-
названием очереди сообщений POSIX. Эти очереди во многом похожи на очере-
очереди сообщений System V IPC, описанные в разд. "Сообщения IPC" ранее в
этой главе. Однако очереди сообщений POSIX обладают рядом преимуществ
над старыми очередями:
□ более простой, основанный на файлах интерфейс прикладного програм-
программирования;
□ встроенная поддержка приоритетов сообщений (приоритет, в конечном
счете, определяет место сообщения в очереди);
□ встроенная поддержка асинхронного уведомления о поступлении сообще-
сообщений либо с помощью сигналов, либо путем создания потока;
□ ограничения по времени для блокирующих операций отправки и приема.
Очереди сообщений POSIX обрабатываются набором библиотечных функ-
функций, перечисленных в табл. 19.15.
Таблица 19.15. Библиотечные функции для очередей сообщений POSIX
Имя функции Описание
mq_open () Открыть (возможно, создав) очередь сообщений POSIX
mq_close () Закрыть очередь сообщений POSIX (не уничтожая ее)
mq_unlink () Уничтожить очередь сообщений POSIX
qsend (), Отправить сообщение в очередь; вторая функция
mq_timedsend () устанавливает временной лимит для операции
mq_receive (), Получить сообщение из очереди; вторая функция
mq_timedreceive () устанавливает временной лимит для операции
mq_notif у () Задействовать механизм асинхронного уведомления
о поступлении сообщений в пустую очередь
mq_getattr (), Соответственно, получить или установить атрибуты очереди
mq_setattr() сообщений POSIX (определяющие, должны ли операции от-
отправления и приема быть блокирующими или неблокирующими)
Рассмотрим типичный случай обращения приложения к этим функциям. На
первом шаге приложение вызывает библиотечную функцию mqopen (), чтобы
открыть очередь сообщений POSIX. Первым аргументом функции является
строка, задающая имя очереди. Оно аналогично имени файла и тоже должно
начинаться с косой черты (/). Функция принимает подмножество флагов сис-
системного вызова open о: o_rdonly, o_wronly, o_rdwr, o_creat, o_excl и
ononblock (для неблокирующих операций отправки и приема сообщений).
Обратите внимание, что приложение может создать новую очередь сообще-
сообщений POSIX, установив флаг ocreat. Функция mq_open () возвращает дескрип-
дескриптор очереди, во многом аналогичный дескриптору файла, возвращаемому
системным вызовом open ().
После того как очередь сообщений POSIX открыта, приложение может от-
отправлять и принимать сообщения, вызывая библиотечные функции mqsend ()
и mqreceive () и передавая им дескриптор очереди, возвращенный методом
mqopeno. Приложение может также воспользоваться функциями
mqtimedsend () И mqtimedreceive (), чтобы Задать максимальное Время, В те-
чение которого приложение будет ждать завершения операции отправки и
приема.
Вместо того чтобы перейти блокироваться в функции mqreceive () (или не-
непрерывно опрашивать очередь сообщений, если задан флаг ononblock), при-
приложение может включить механизм асинхронного уведомления, вызвав биб-
библиотечную функцию mqnotifyO. Приложение может потребовать, чтобы,
когда в пустую очередь поступит сообщение, был послан сигнал указанному
процессу или был создан новый поток.
И наконец, когда приложение перестанет пользоваться очередью сообщений,
оно вызовет библиотечную функцию mqciose (), передав ей дескриптор оче-
очереди. Обратите внимание, что эта функция не уничтожает очередь, подобно
тому, как системный вызов close о не удаляет файл. Чтобы уничтожить оче-
очередь, Приложение ДОЛЖНО ВЫЗВаТЬ фуНКЦИЮ mq_unlink () .
Реализация очередей сообщений POSIX в Linux 2.6 проста и понятна. Введе-
Введена специальная файловая система mqueue (см. разд. "Специальные файловые
системы" главы 12), которая содержит индексные дескрипторы для
всех существующих очередей. Ядро предоставляет несколько системных вы-
вызовов, примерно соответствующих библиотечным функциям из табл. 19.15:
mq_open(), mq_unlink(), mq_timedsend (), mq_timedreceive (), mq_notify() И
mqgetsetattro. Эти системные вызовы прозрачным образом воздействуют
на файлы файловой системы mqueue, т. е. основная работа ложится на слой
виртуальной файловой системы. Обратите внимание, что ядро не предлагает
функцию mqciose(): действительно, дескриптор очереди, возвращенный
приложению, фактически является дескриптором файла, и библиотечная
функция mqcioseo может просто сделать системный вызов close о, чтобы
выполнить свою задачу.
Специальная файловая система mqueue не обязательно должна быть смонти-
смонтирована в дереве каталогов системы. Однако если она смонтирована, пользо-
пользователь может создать очередь сообщений POSIX, создав файл в корневом
каталоге этой файловой системы. Кроме того, он может получить информа-
информацию об очереди, прочитав соответствующий файл. Наконец, приложение мо-
может сделать системные вызовы select о и poll о, чтобы получить уведомле-
уведомление об изменении состояния очереди.
Каждая очередь описывается дескриптором mqueueinodeinfo, который
включает в себя объект "индексный дескриптор", ассоциированный с соот-
соответствующим файлом в специальной файловой системе mqueue. Когда сис-
системный вызов, имеющий отношение к очереди сообщений POSIX, принимает
дескриптор очереди в качестве параметра, он вызывает функцию fget () вир-
виртуальной файловой системы, чтобы получить адрес необходимого файлового
объекта. Затем системный вызов получает объект "индексный дескриптор"
файла в файловой системе mqueue и адрес дескриптора mqueueinodeinf о,
содержащего объект "индексный дескриптор".
Сообщения, находящиеся в очереди, организованы в однонаправленный спи-
список с головой в дескрипторе mqueueinodeinf о. Каждое сообщение представ-
представлено дескриптором типа msgmsg, точно таким же, какой используется для со-
сообщения System V IPC.
ГЛАВА 20
Выполнение программ
Понятие процесса, обсуждавшееся в главе 3, используется в операционной
системе Unix с момента ее возникновения для представления поведения
группы программ, конкурирующих за системные ресурсы. Эта заключитель-
заключительная глава посвящена взаимосвязи между процессом и программой. Мы особо
опишем, как ядро устанавливает контекст выполнения для процесса в соот-
соответствии с содержимым файла программы. Казалось бы, невелика пробле-
проблема— загрузить в память какое-то количество инструкций и указать цен-
центральному процессору, что их надо выполнить. Тем не менее ядру приходит-
приходится проявлять гибкость, работая сразу в нескольких направлениях:
□ Различные форматы исполняемых файлов — Linux отличается своим уме-
умением выполнять двоичный код, откомпилированный под другие операци-
операционные системы. В частности, система Linux способна выполнить код, соз-
созданный для 32-разрядной машины, на 64-разрядной версии той же маши-
машины. Например, исполняемый файл, созданный на Pentium, может быть
выполнен на 64-разрядном AMD Opteron.
□ Совместно используемые библиотеки— многие исполняемые файлы не
содержат весь код, необходимый для работы программы, а ожидают, что
ядро загрузит функции из библиотеки во время выполнения.
□ Прочая информация в контексте выполнения— сюда входят знакомые
каждому программисту аргументы командной строки и переменные окру-
окружения.
Программа хранится на диске в исполняемом файле, который содержит как
объектный код функций, подлежащих выполнению, так и данные, необходи-
необходимые этим функциям для работы. Многие функции программы являются слу-
служебными, доступными всем программистам. Их объектный код включен в
состав специальных файлов, называемых библиотеками. На практике код
библиотечной функции может быть либо статически скопирован в исполняе-
исполняемый файл (тогда говорят о статических библиотеках), либо скомпонован с
процессом на этапе выполнения (в этом случае речь идет о совместно исполь-
используемых библиотеках, потому что их кодом могут одновременно пользоваться
несколько независимых процессов).
При запуске программы пользователь может предоставить два вида инфор-
информации, влияющей на способ выполнения программы: аргументы командной
строки и переменные окружения. Аргументы командной строки вводятся
пользователем с терминала после приглашения оболочки, вслед за именем
исполняемого файла. Переменные окружения, например, номе или path, пре-
предоставляются оболочкой, но пользователи могут модифицировать их значе-
значения до запуска программы.
В разд. "Исполняемые файлы" мы поясним, что такое контекст выполнения
программы. В разд. "Форматы исполняемых файлов" мы упомянем некото-
некоторые форматы двоичных программ, поддерживаемые операционной системой
Linux, и продемонстрируем, как она может менять свое "лицо", чтобы выпол-
выполнять программы, откомпилированные для других операционных систем. На-
Наконец, в разд. "Функции exec" мы опишем системный вызов, который позво-
позволяет процессу начать выполнение новой программы.
Исполняемые файлы
В главе 1 процесс был определен как "контекст выполнения". Под этим мы
понимаем весь объем информации, необходимой для проведения конкретных
вычислений. Сюда входят страницы памяти, открытые файлы, содержимое
аппаратных регистров и т. д. Исполняемый файл — это обычный файл, кото-
который описывает, как инициализировать новый контекст выполнения (то есть
как начать новые вычисления). Предположим, пользователь хочет получить
список файлов в текущем каталоге. Он знает, что для этого достаточно ука-
указать имя файла /bin/Is1 внешней команды после приглашения оболочки.
Командная оболочка ответвляет новый процесс, который делает системный
вызов execveO (см. разд. "Функции exec" далее в этой главе), передавая ему
среди прочих параметров строку с полным путем к исполняемому файлу Is,
в данном случае /bin/Is. Служебная процедура sysexecve () находит соответ-
1 Пути к конкретным исполняемым файлам не стандартизированы в Linux и зависят от дистрибути-
дистрибутива. Для операционных систем семейства Unix было предложено несколько схем организации файло-
файловой системы, например, FHS (Filesystem Hierarchy Standard, Стандарт для иерархической структуры
файловой системы).
ствующий файл, определяет его формат и модифицирует контекст выполне-
выполнения текущего процесса в соответствии с полученной информацией. Когда
системный вызов заканчивает работу, процесс приступает к выполнению ко-
кода, хранящегося в исполняемом файле, в результате чего выводится содержа-
содержание каталога.
Когда процесс запускает новую программу, его контекст выполнения изменя-
изменяется очень сильно, потому что большая часть ресурсов, полученных на пре-
предыдущем этапе вычислений, отбрасывается. В нашем примере, когда процесс
начинает выполнение программы /bin/Is, он заменяет аргументы оболочки на
те, что были переданы системному вызову execve (), и получает новое окру-
окружение оболочки (см. разд. "Аргументы командной строки и окружение обо-
оболочки" далее в этой главе). Все страницы, унаследованные от процесса-
родителя (и совместно используемые с помощью механизма копирования при
записи), освобождаются, чтобы новые вычисления начались в "свежем" ад-
адресном пространстве режима пользователя; более того, даже привилегии
процесса могут измениться (см. разд. "Права и способности процесса" далее
в этой главе). Впрочем, идентификатор процесса остается прежним, и новые
вычисления наследуют от предыдущих все дескрипторы открытых файлов,
которые не были автоматически закрыты системным вызовом execve (J.
Права и способности процесса
По традиции, в Unix-подобных системах с каждым процессом ассоциируются
определенные права, которые привязывают процесс к конкретному пользова-
пользователю и конкретной группе пользователей. Права играют важную роль в мно-
многопользовательских системах, поскольку определяют, что процесс может де-
делать, а чего он не может. В результате сохраняется как целостность данных,
принадлежащих каждому пользователю, так и стабильность системы в целом.
Наличие прав требует поддержки как в структуре данных процесса, так и в
защищаемых ресурсах. Самым очевидным ресурсом является файл. Напри-
Например, в файловой системе Ext2 каждый файл принадлежит конкретному поль-
пользователю и привязан к некоторой группе пользователей. Владелец файла мо-
может решать, какие операции над файлом разрешены для него, владельца, для
группы пользователей файла и для всех остальных пользователей. Когда про-
процесс пытается обратиться к файлу, виртуальная файловая система обязатель-
обязательно проверяет допустимость обращения с точки зрения прав доступа, установ-
установленных владельцем файла, и прав процесса.
2 По умолчанию файл, открытый процессом, остается открытым и после системного вызова
execve (). Однако он автоматически закрывается, если процесс установил нужный бит в поле
close_on_exec структуры f iles_struct при помощи системного вызова f cntl ().
Права процесса хранятся в нескольких полях его дескриптора, перечислен-
перечисленных в табл. 20.1. Эти поля содержат идентификаторы пользователей и поль-
пользовательских групп в системе, и эти идентификаторы обычно сравниваются с
теми, что хранятся в индексных дескрипторах файлов, к которым процесс
пытается обратиться.
Таблица 20.1. Традиционные права процесса
Имя Описание
uid, gid Реальные идентификаторы пользователя и группы
euid, egid Эффективные идентификаторы пользователя и группы
f suid, f sgid Эффективные идентификаторы пользователя и группы для обращения
к файлам
groups Дополнительные идентификаторы группы
suid, sgid Сохраненные идентификаторы пользователя и группы
Идентификатор пользователя, равный 0, определяет суперпользователя
(пользователя root), а идентификатор группы, равный 0, определяет группу
root. Если какое-то право процесса тоже равно 0, ядро обходит проверки и
позволяет привилегированному процессу выполнять различные действия,
например, относящиеся к системному администрированию или манипуляци-
манипуляциям с аппаратурой, непозволительные непривилегированным процессам.
Когда процесс создается, он наследует права своего родителя. Однако впо-
впоследствии они могут быть изменены, либо когда процесс начнет выполнять
новую программу, либо если он сделает нужный системный вызов. Обычно в
полях uid, euid, f suid и suid дескриптора процесса хранится одно и то же
значение. Когда процесс выполняет программу с установкой идентификато-
идентификатора пользователя, т. е. запускает исполняемый файл, у которого установлен
флаг setuid, в поля euid и f suid записывается идентификатор владельца фай-
файла. Почти все проверки затрагивают одно из следующих двух полей: f suid
проверяется при операциях, относящихся к файлам, а поле euid — при всех
остальных операциях. То же самое можно сказать и о полях gid, egid, f sgid и
sgid, в которых хранятся идентификаторы группы.
В качестве иллюстрации работы с полем f suid рассмотрим типичную ситуа-
ситуацию, в которой пользователь хочет сменить свой пароль. Все пароли хранятся
в общем файле, но пользователь не может напрямую отредактировать его,
потому что файл защищен. Поэтому пользователь вызывает системную про-
программу /usr/bin/passwd, у которой флаг setuid установлен, и чьим владельцем
является суперпользователь. Когда процесс, ответвленный оболочкой, вы-
выполняет эту программу, его поля euid и f suid становятся равны 0, т. е. идеи-
тификатору процесса суперпользователя. Теперь процесс может обратиться к
файлу, потому что, когда ядро выполняет контроль доступа, оно находит 0 в
поле fsuid. Конечно, программа /usr/bin/passwd не позволит пользователю
ничего, кроме смены его пароля.
Долгая история Unix учит нас, что программы с установкой идентификатора
пользователя (то есть программы, у которых установлен флаг setuid), весьма
опасны. Злоумышленник может воспользоваться какими-нибудь ошибками в
коде такой программы и заставить ее выполнять операции, не запланирован-
запланированные ее создателями. В худшем случае может быть разрушена вся система
безопасности. Чтобы минимизировать такой риск, Linux, подобно всем со-
современным системам семейства Unix, дает процессам привилегии только в
случае необходимости и отбирает их, как только нужда в привилегиях отпа-
отпала. Эта особенность может оказаться весьма кстати при реализации приложе-
приложений с несколькими уровнями защиты. Дескриптор процесса имеет поле suid,
в котором хранятся значения эффективных идентификаторов (euid и fsuid) на
момент запуска программы с установкой идентификатора пользователя. Про-
Процесс может изменить эффективные идентификаторы с помощью системных
ВЫЗОВОВ setuid (), setresuid (), setf suid () И setreuid () .
В табл. 20.2 показано, как эти системные вызовы влияют на права процесса.
При изучении таблицы необходимо отдавать себе отчет: если вызвавший
процесс не имеет привилегий суперпользователя, т. е. если его поле euid не
равно нулю, то эти системные вызовы могут быть использованы только для
установки значений, уже имеющихся в полях с правами процесса. Например,
"среднестатистический" пользовательский процесс может записать значе-
значение 500 В СВОе ПОЛе fsuid При ПОМОЩИ СИСТеМНОГО ВЫЗОВа setfsuid(), ТОЛЬКО
если в каком-то другом поле с правами уже находится это значение.
Таблица 20.2. Семантика системных вызовов, устанавливающих права процесса
setuid(е)
Поле 1 setresuid(u,e,s) setreuid (u, e) setf suid (f)
euid=0 eui<±^0
uid Устанавли- Не меняется Устанавливается в Устанавлива- Не меняется
вается значение и ется в значе-
в значение е ние и
euid Устанавли- Устанавли- Устанавливается в Устанавлива- Не меняется
вается вается в зна- значение е ется в значе-
в значение е чение е ние е
3 Эффективные идентификаторы группы можно изменить с помощью системных вызовов
setgid(), setresgid(), setfsgid() и setregid().
Таблица 20.2 (окончание)
setuid (e)
Поле 1 setresuid(u,e,s) setreuid(u,e) setfsuid(f)
euid=0 euid^O
fsuid Устанавли- Устанавли- Устанавливается в Устанавлива- Устанавли-
Устанавливается вается в зна- значение е ется в значе- вается в
в значение е чение е ние е значение f
suid Устанавли- Не меняется Устанавливается в Устанавлива- Не меняется
вается значение s ется в значе-
в значение е ние е
Чтобы разобраться в порой непростых отношениях между четырьмя полями с
идентификаторами пользователя, рассмотрим действие системного вызова
setuid (). Оно различно в зависимости от того, равно ли нулю поле euid про-
процесса, сделавшего вызов (имеет ли процесс привилегии суперпользователя),
или оно содержит идентификатор обычного пользователя.
Если поле euid равно 0, системный вызов записывает значение параметра е во
все поля, указывающие права вызвавшего процесса (uid, euid, f suid и suid).
Таким образом, суперпользовательский процесс может отказаться от своих
привилегий и стать процессом, принадлежащим обычному пользователю. Это
происходит, например, когда пользователь входит в систему: система ответв-
ответвляет новый процесс с привилегиями суперпользователя, но он отказывается
от них с помощью системного вызова setuid () и затем выполняет программу
оболочки, указанную для текущего пользователя.
Если поле euid не равно 0, системный вызов setuid () изменяет только значе-
значения в полях euid и f suid, не затрагивая другие два поля. Такое поведение сис-
системного вызова полезно при реализации программы с установкой идентифи-
идентификатора пользователя, которая расширяет или сужает эффективные привиле-
привилегии процесса, хранящиеся в полях euid и f suid.
Способности процесса
Проект POSIX.le, ныне отозванный, ввел другую модель прав процесса, ос-
основанную на понятии "способностей". Ядро Linux поддерживает способности
процесса POSIX, хотя большинство дистрибутивов их игнорирует.
Способность — это просто флаг, который определяет, разрешено ли процес-
процессу выполнять конкретную операцию или конкретный класс операций. Эта
модель отлична от традиционной модели "суперпользователь против обычно-
обычного пользователя", в которой процесс может либо все, либо ничего, в зависи-
зависимости от его эффективного идентификатора пользователя. Как показано в
табл. 20.3, ряд способностей процесса поддерживается ядром Linux.
Таблица 20.3. Способности процесса в Linux
Имя Описание
cap_audit_write Разрешить генерацию аудиторских сообщений путем записи
в сокеты netlink
cap_audit_control Разрешить контроль аудиторских действий ядра с помощью
сокетов netlink
cap_chown Игнорировать ограничения по изменению прав на владение
флагом со стороны пользователя и группы
cap_dac_override Игнорировать права доступа к файлу
cap_dac_read_search Игнорировать права на чтение и поиск файла/каталога
cap_fowner Вообще игнорировать проверки прав на владение файлом
cap_fsetid Игнорировать ограничения по установке флагов setuid
и setgid для файлов
capkill Обходить проверки ограничений при генерировании сигналов
cap_linux_immutable Разрешить модификацию постоянных файлов и файлов
"только для дозаписи" в файловых системах Ext2 и Ext3
cap_ipc_lock Разрешить блокировку страниц и совместно используемых
сегментов памяти
cap_ipc_owner Пропускать проверку владельца при межпроцессном взаимо-
взаимодействии
cap_lease Разрешить блокировку "аренда файла" (см.
разд. "Блокировка файлов в Linux" главы 12)
cap_mknod Разрешить привилегированные операции mknod ()
cap_net_admin Разрешить общее сетевое администрирование
cap_net_bind_service Разрешить привязку сокетов TCP/UDP к портам ниже 1024
cap_net_broadcast Разрешить широковещательную и многоадресную рассылку
capnetraw Разрешить использование сокетов RAW и PACKET
cap_setgid Игнорировать ограничения на манипуляции с правами про-
процесса, относящимися к группе пользователей
cap_setpcap Разрешить манипуляции со способностями других процессов
cap_setuid Игнорировать ограничения на манипуляции с правами про-
процесса, относящимися к пользователю
cap_sys_admin Разрешить общее системное администрирование
cap_sys_boot Разрешить вызов reboot ()
cap_sys_chroot Разрешить вызов chroot ()
cap_sys_module Разрешить загрузку и выгрузку модулей ядра
Таблица 20.3 (окончание)
Имя Описание
capsysnice Пропустить проверку прав доступа для системных вызовов
nice () и setpriority () и позволить создавать процессы
реального времени
capsyspacct Разрешить настройку попроцессного учета
capsysptrace Разрешить вызов ptrace () для любого процесса
capsysrawio Разрешить доступ к портам ввода/вывода с помощью
системных вызовов iopermO и iopl ()
capsysresource Разрешить поднятие лимитов на ресурсы
capsystime Разрешить манипуляции с системными часами и часами
реального времени
capsysttyconfig Разрешить настройку терминала и применение системного
вызова vhangup ()
Основное достоинство способностей процесса состоит в том, что в любой
момент времени конкретной программе требуется лишь их ограниченное
подмножество. Следовательно, если злоумышленник откроет способ экс-
эксплуатировать ошибки в программе, он сможет незаконно выполнить лишь
ограниченный набор операций.
Предположим, например, что уязвимая программа имеет только способность
capsystime. В этом случае злоумышленник, научившийся эксплуатировать
уязвимость, сумеет лишь нелегально перевести системные часы и часы ре-
реального времени. Никакая другая привилегированная операция не будет ему
доступна.
В настоящее время ни виртуальная файловая система, ни файловая система
Ext2 не поддерживают модель способностей процесса, и, следовательно, не-
невозможно ассоциировать исполняемый файл с набором способностей, всту-
вступающих в силу, когда процесс выполняет этот файл. Тем не менее процесс
может явно расширить или сузить свои способности с помощью системных
вызовов capgeto и capset () соответственно. Например, есть возможность
модифицировать программу login так, что она сохранит часть своих способ-
способностей и утратит остальные.
Ядро Linux уже учитывает способности процессов. Рассмотрим в качестве
примера системный вызов nice о, позволяющий пользователям изменять ста-
статический приоритет процесса. В традиционной модели только суперпользо-
суперпользователю разрешено повышать приоритет, и ядро должно проверять, содержит-
содержится ли ноль в поле euid дескриптора процесса, сделавшего системный вы-
вызов. Однако в ядре Linux определена способность процесса, называемая
capsysnice, которая соответствует именно этой операции. Ядро проверяет
значение флага, вызывая функцию capable о и передавая ей значение
CAP_SYS_NICE.
Такой подход работает благодаря "трюкам совместимости", примененным в
коде ядра: всякий раз, когда процесс обнуляет поля euid и f suid (либо с по-
помощью системных вызовов, перечисленных в табл. 20.2, либо выполняя про-
программу с установкой идентификатора пользователя, принадлежащую супер-
суперпользователю), ядро устанавливает все способности процесса, и все проверю!
проходят успешно. Когда процесс восстанавливает в полях euid и fsuid ре-
реальный идентификатор пользователя-владельца процесса, ядро проверяет
флаг keepcapabiiities у дескриптора процесса и лишает процесс всех спо-
способностей, если флаг сброшен. Процесс может устанавливать и сбрасывать
флаг keepcapabiiities, делая специфичный для Linux системный вызов
prctl().
Программный каркас Linux Security Modules
В Linux 2.6 способности процессов тесно интегрированы в программный кар-
каркас LSM (Linux Security Modules, Модули безопасности Linux). По сути, про-
программный каркас LSM позволяет разработчикам определять несколько аль-
альтернативных моделей безопасности ядра.
Каждая модель безопасности реализована набором перехватчиков. Пере-
Перехватчик — это функция, вызываемая ядром, когда оно собирается выполнить
важную операцию, затрагивающую безопасность системы. Функция-
перехватчик определяет, следует ли выполнить или отклонить операцию.
Перехватчики хранятся в таблице типа securityoperations. Адрес такой таб-
таблицы для модели безопасности, используемой в данный момент, хранится в
переменной securityops. По умолчанию ядро применяет модель минималь-
минимальной безопасности, реализованную в таблице dmnmy_security_ops. Каждый пе-
перехватчик в этой таблице проверяет способность процесса, если таковая име-
имеется, или безусловно возвращает 0 (операция разрешена).
Например, служебные процедуры функций stimeo и settimeofday () вызыва-
вызывает перехватчик settime перед изменением даты и времени в системе. Соот-
Соответствующая функция, указатель на которую хранится в таблице
dummysecurityops, ограничивает себя проверкой, установлен ли у текущего
процесса флаг capsystime, и возвращает 0 или -eperm в зависимости от ре-
результата.
Для Linux были разработаны довольно сложные модели безопасности. Са-
Самым известным примером является SELinux (Security-Enhanced Linux, "Linux
с усиленной безопасностью"), созданный Национальным агентством безо-
безопасности США (National Security Agency).
Аргументы командной строки
и окружение оболочки
Когда пользователь вводит команду с клавиатуры, программа, загруженная
для удовлетворения запроса, может получить от оболочки несколько аргу-
аргументов командной строки. Например, если пользователь ввел:
$ Is -I /usr/bin
и хочет получить полный список файлов в каталоге /usr/bin, процесс оболоч-
оболочки создает новый процесс для выполнения команды. Этот новый процесс за-
загружает исполняемый файл /bin/Is. В результате большая часть контекста вы-
выполнения, унаследованного от оболочки, теряется, но три аргумента is, -l и
/usr/bin сохраняются. Вообще говоря, новый процесс может получить любое
количество аргументов.
Соглашения по передаче аргументов командной строки зависят от исполь-
используемого языка высокого уровня. В языке С функция main () любой программы
может принимать в качестве параметров целое, задающее количество аргу-
аргументов командной строки и указатель на массив строк. Этот стандарт форма-
лизируется следующим прототипом:
int main(int argc, char *argv[])
Вернемся к нашему примеру. Когда вызывается программа /bin/Is, параметр
argc имеет значение 3, элемент массива argv[0] указывает на строку is, эле-
элемент argv[i] — на строку -1, а элемент argv[2] — на строку /usr/bin. Конец
массива argv всегда обозначается нулевым указателем, так что элемент
argv[3] СОДерЖИТ NULL.
Третий, необязательный параметр, который может быть передан функции
main о в языке С, содержит переменные окружения. Они служат для настрой-
настройки контекста выполнения процесса, для передачи общей информации пользо-
пользователю или другим процессам или для того, чтобы позволить процессу со-
сохранить некоторую информацию после выполнения системного вызова
execve().
Чтобы воспользоваться переменными окружения, можно объявить функцию
main () следующим образом:
int main(int argc, char *argv[], char *envp[])
Параметр envp указывает на массив указателей на строки, имеющие следую-
следующий вид:
ИМЯ_ПЕРЕМЕННОЙ=значение
Здесь имяпеременной представляет имя переменной окружения, а подстрока,
следующая за знаком равенства, представляет фактическое значение, присво-
енное переменной. Конец массива envp обозначается нулевым указателем, как
и у массива argv. Адрес массива envp хранится также в глобальной перемен-
переменной environ, определенной в библиотеке С.
Строки окружения и аргументы командной строки хранятся в стеке режима
пользователя, непосредственно перед адресом возврата (см. главу 10). Ниж-
Нижние позиции стека режима пользователя изображены на рис. 20.1. Обратите
внимание, что переменные окружения расположены у дна стека, сразу после
длинного целого 0.
Рис. 20.1. Нижние позиции стека режима пользователя
Библиотеки
Каждый файл с исходным кодом на языке высокого уровня путем ряда пре-
преобразований превращается в объектный файл, который содержит машинный
код инструкций ассемблера, соответствующих инструкциям высокого уров-
уровня. Объектный файл нельзя выполнять, потому что он не содержит линейных
адресов, соответствующих ссылкам на имена глобальных символов, внешних
по отношению к файлу с исходным кодом, каковыми являются, например,
функции в библиотеках или другие файлы с исходным кодом той же про-
программы. Присваивание, или разрешение, таких адресов выполняется компо-
компоновщиком, который собирает исполняемый файл. Кроме того, компоновщик
определяет, какие библиотечные функции нужны программе, и добавляет их
к исполняемому файлу способом, описанным далее в этой главе.
Большинство программ, даже самых простых, пользуется библиотеками.
В качестве примера возьмем программу на языке С, состоящую из одной
строчки:
void main(void) { }
Хотя эта программа не производит никаких вычислений, необходимо проде-
проделать большую работу по настройке ее окружения (см. разд. "Функции exec"
далее в этой главе) и по уничтожению процесса, когда программа завершится
(см. главу 3). В частности, после окончания функции main о компиляторе
вставляет в объектный код вызов функции exitgroup ().
Из главы 10 мы знаем, что программы обычно делают системные вызовы по-
посредством интерфейсных функций из библиотеки С. Это справедливо и в от-
отношении компилятора С. Кроме кода, непосредственно сгенерированного в
результате компиляции операторов программы, каждый исполняемый файл
содержит и некоторое количество "добавочного" кода, обеспечивающего
взаимодействие процессов режима пользователя с ядром. Порции такого ко-
кода хранятся в библиотеке С.
В системах семейства Unix существует много других библиотек функций,
кроме библиотеки С. Типичная система Linux, как правило, имеет несколько
сотен библиотек. Пара примеров: математическая библиотека libm содержит
функции для операций с плавающей точкой, а библиотека НЬХП является
собранием базовых низкоуровневых функций для графического интерфейса
XII Window System.
В традиционных Unix-системах исполняемые файлы основывались на ста-
статических библиотеках. Это означает, что исполняемый файл, созданный
компоновщиком, содержит не только код самой программы, но и код библио-
библиотечных функций, которые она вызывает. Большим недостатком статически
скомпонованных программ является то, что они "съедают" уйму места на
диске. В самом деле, каждая статически скомпонованная программа дублиру-
дублирует какую-то порцию библиотечного кода.
В современных системах Unix применяются совместно используемые биб-
библиотеки. Исполняемый файл содержит не объектный код из библиотеки, а
только ссылку на имя библиотеки. Когда программа загружается в память для
выполнения, специальная программа, называемая динамическим компонов-
компоновщиком (или ld.so), анализирует имена библиотек в исполняемом файле, ищет
библиотеки в системном дереве каталогов и делает запрошенный код доступ-
доступным выполняющемуся процессу. Кроме того, процесс может загрузить до-
дополнительные совместно используемые библиотеки на этапе выполнения,
вызвав библиотечную функцию diopen ().
Совместно используемые библиотеки особенно удобны в системах с под-
поддержкой отображения файлов в память, поскольку они сокращают объем
оперативной памяти, необходимый для выполнения программы. Когда дина-
динамический компоновщик должен подключить к процессу совместно исполь-
используемую библиотеку, он не копирует объектный код, а выполняет лишь ото-
отображение необходимой порции библиотечного файла в адресное пространст-
пространство процесса. Это позволяет всем процессам, использующим один и тот же
библиотечный код, обращаться к страничным кадрам, которые его содержат.
Очевидно, что такое невозможно, если программа была скомпонована стати-
статически.
Совместно используемые библиотеки имеют и недостатки. Время запуска
динамически скомпонованной программы, как правило, больше, чем у ском-
скомпонованной статически. Кроме того, динамически скомпонованные програм-
программы не так легко переносить в другие системы, как статически скомпонован-
скомпонованные, потому что они могут перестать работать в системе, имеющей другую
версию той же библиотеки.
У пользователя всегда есть возможность потребовать статическую компонов-
компоновку программы. Например, компилятор GCC предлагает опцию -static, кото-
которая заставляет компоновщик пользоваться статическими, а не совместно ис-
используемыми библиотеками.
Сегменты программы и области памяти процесса
По традиции линейное адресное пространство программ в Unix логически
разделено на несколько интервалов линейных адресов, называемых сегмен-
сегментами4:
□ Сегмент текста — содержит исполняемый код программы.
□ Сегмент инициализированных данных— содержит инициализированные
данные, т. е. статические и глобальные переменные, начальные значения
которых хранятся в исполняемом файле (потому что программе необхо-
необходимы их значения уже при запуске).
□ Сегмент неинициализированных данных (bss) — содержит неинициализи-
неинициализированные данные, т. е. все глобальные переменные, начальные значения
которых не хранятся в исполняемом файле (потому что программа уста-
устанавливает их значения до обращения к ним). По традиции этот сегмент на-
называется bss-сегментом.
О Сегмент стека — содержит программный стек, в котором хранятся адре-
адреса возврата, параметры и локальные переменные вызываемых функций.
4 Термин "сегмент" имеет исторические корни. В ранних системах Unix каждый интервал линей-
линейных адресов был реализован с помощью отдельного сегментного регистра. Однако Linux не исполь-
использует механизм сегментации микропроцессоров 80x86 при реализации сегментов программы.
Каждый дескриптор памяти mmstruct (см. главу 9) имеет несколько полей,
определяющих важнейшие области памяти соответствующего процесса:
□ startcode, endcode — содержат начальный и конечный линейные адреса
области памяти, включающей в себя оригинальный код программы, т. е.
код из исполняемого файла;
□ startdata, enddata — содержат начальный и конечный линейные адреса
области памяти, включающей в себя оригинальные инициализированные
данные программы, как они определены в исполняемом файле. Эти поля
идентифицируют область памяти, приблизительно соответствующую сег-
сегменту данных;
□ startbrk, brk — содержат начальный и конечный линейные адреса облас-
области памяти, включающей в себя динамически выделенные процессу участ-
участки памяти (см. главу 9). Эту область памяти иногда называют кучей;
П startstack — содержит адрес, расположенный непосредственно над ад-
адресом возврата функции main о. Как видно из рис. 20.1, более высокие ад-
адреса зарезервированы (вспомним, что стек растет в направлении уменьше-
уменьшения адресов);
□ argstart, argend — содержат начальный и конечный адреса части стека,
в которой хранятся аргументы командной строки;
□ envstart, envend — содержат начальный и конечный адреса части стека,
в которой хранятся строки переменных окружения.
Обратите внимание, что с появлением совместно используемых библиотек и
отображения файлов в память классификация адресного пространства про-
процесса, основанная на сегментах программы, морально устарела. Каждая со-
совместно используемая библиотека отображается в область памяти, не имею-
имеющую отношения к упомянутым.
Гибкая схема расположения областей памяти
В версии ядра 2.6.9 была введена гибкая схема расположения областей памя-
памяти. Каждый процесс получает память в зависимости от ожидаемого роста
стека режима пользователя. Впрочем, классическая схема тоже может быть
использована (в основном, когда ядро не в состоянии ограничить размер сте-
стека процесса в режиме пользователя). В табл. 20.4 описаны обе схемы для
архитектуры 80x86, в предположении, что адресное пространство режима
пользователя по умолчанию простирается до 3 Гбайт.
Как видно из таблицы, схемы различаются только в расположении областей
памяти, предназначенных для отображений файлов и для анонимных отобра-
отображений. В классической схеме эти области помещаются, начиная с одной тре-
ти всего адресного пространства режима пользователя, обычно с адреса
Ох4ооооооо. Новые области добавляются в более высокие линейные адреса,
т. е. области расширяются в направлении стека режима пользователя.
Таблица 20А. Схема областей памяти в архитектуре 80*86
Сегмент текста (ELF) Начинается с адреса 0x08048000
Сегмент данных и bss Начинается сразу после сегмента текста
Куча Начинается сразу после сегментов данных и bss
Отображения файлов Начинается с адреса 0x40000000 Начинается вблизи конца
в память и анонимные (что соответствует 1/3 всего (самого нижнего адреса)
области памяти адресного пространства режима стека режима пользова-
пользователя); библиотеки теля; библиотеки после-
последовательно добавляются довательно добавляются
в более высокие адреса в более низкие адреса
Стек режима пользо- Начинается с адреса ОхсООООООО и растет в сторону уменьше-
вателя ния адресов
В гибкой схеме, наоборот, области памяти для отображений файлов и ано-
анонимных отображений помещаются вблизи конца стека. Новые области до-
добавляются в более низкие линейные адреса, т. е. области расширяются в на-
направлении кучи. Вспомним, что стек также растет в сторону понижения ад-
адресов.
Ядро, как правило, прибегает к гибкой схеме, когда оно может получить пре-
предельный размер стека режима пользователя от ограничителя ресурсов
rlimitstack (см. главу 3). Этот ограничитель определяет размер пространст-
пространства линейных адресов, зарезервированного под стек, причем размер не может
быть меньше 128 Мбайт или больше 2.5 Гбайт.
С другой стороны, если либо предел rlimitstack установлен "на бесконеч-
бесконечность", либо системный администратор приравнял к единице переменную
sysctiiegacyvaiayout (записав соответствующее значение в файл
/proc/sys/vm/legacy_va_layout или сделав системный вызов sysctio), ядро не
может определить верхнюю границу размера стека. В этом случае оно поль-
пользуется классической схемой расположения областей памяти.
С какой целью была введена гибкая схема? Ее главное достоинство в том, что
она позволяет процессу более эффективно распоряжаться пространством ли-
линейных адресов режима пользователя. В классической схеме размер кучи ог-
ограничен менее чем одним гигабайтом, а другие области памяти могут занять
до 2 Гбайт (минус размер стека). В гибкой схеме эти ограничения снимаются:
как кучи, так и остальные области памяти могут свободно расширяться, пока
не захватят все линейные адреса, не использованные стеком и сегментами
программы, имеющими фиксированные размеры.
А сейчас поставим небольшой, но поучительный эксперимент. Напишем и
скомпилируем такую программу на языке С:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
char cmd[32];
brk((void *Hx8051000);
sprintf(cmd, "cat /proc/self/maps");
system (cmd) ;
return 0;
}
Программа увеличивает кучу процесса (см. главу 9), а затем читает файл maps
специальной файловой системы /ргос и выводит список областей памяти
процесса.
Запустим программу, не установив ограничений на размер стека:
# ulimit -s unlimited; /tmp/memorylayout
08048000-08049000 r-xp 00000000 03:03 5042408 /tmp/memorylayout
08049000-0804a000 rwxp 00000000 03:03 5042408 /tmp/memorylayout
0804a000-08051000 rwxp 0804a000 00:00 0
40000000-40014000 r-xp 00000000 03:03 620801 /lib/ld-2.3.2.so
40014000-40015000 rwxp 00013000 03:03 620801 /lib/ld-2.3.2.so
40015000-40016000 rwxp 40015000 00:00 0
4002f000-40157000 r-xp 00000000 03:03 620804 /lib/libc-2.3.2.so
40157000-4015b000 rwxp 00128000 03:03 620804 /lib/libc-2.3.2.so
4015b000-4015e000 rwxp 4015b000 00:00 0
bffeb000-c0000000 rwxp bffebOOO 00:00 0
ffffeOOO-fffffOOO p 00000000 00:00 0
(Вы, возможно, получите немного иную таблицу, в зависимости от версии
компилятора С и того, как была скомпонована программа.) Первые два шест-
надцатеричных числа показывают область памяти, за ними идут флаги прав
доступа, далее — информация о файле, отображенном в область (если тако-
таковой имеется): начальное смещение внутри файла, номер блочного устройства,
номер индексного дескриптора и путь к файлу.
Обратите внимание, что все области памяти реализованы закрытыми отобра-
отображениями (буква р в колонке разрешений). Это неудивительно, поскольку об-
области существуют только для хранения данных процесса. Выполняя инструк-
инструкции, процесс может модифицировать содержимое этих областей, но файлы,
ассоциированные с ними, остаются неизменными. Именно так и работает за-
закрытое отображение в память.
Область памяти, начинающаяся с адреса 0x8048000, является отображением,
ассоциированным с порцией файла /tmp/memorylayout от байта 0 до бай-
байта 4095. Права доступа указывают, что область является выполняемой (ведь
она содержит объектный код), доступна только для чтения (в нее нельзя за-
записывать, потому что инструкции не меняются по ходу выполнения) и явля-
является закрытой. Все правильно, потому что область отображает сегмент тек-
текста программы.
Область памяти, начинающаяся с адреса 0x8049000— это еще одно отобра-
отображение. Оно ассоциировано с той же порцией файла /tmp/memorylayout от
байта 0 до байта 4095. Наша программа так мала, что все ее сегменты (сег-
(сегмент текста, сегмент данных и сегмент bss) находятся на одной странице
файла. Поэтому область памяти, содержащая сегменты данных и bss пере-
перекрывается с предыдущей областью в пространстве линейных адресов.
Третья область памяти содержит кучу процесса. Заметьте, что она заканчива-
заканчивается на линейном адресе 0x8051000, который был передан системному вызову
brk().
Следующие две области памяти, которые начинаются с адресов Ох4ооооооо и
0x40014000, содержат сегменты динамического компоновщика (сегмент ntrcnf
и сегменты данных и bss соответственно) для совместно используемых ELF-
библиотек. В этой системе он носит имя /lib/ld-2.3.2.so. Динамический ком-
компоновщик никогда не выполняется сам по себе; он обязательно отображается
в адресное пространство процесса, выполняющего другую программу. Ано-
Анонимная область памяти, начинающаяся с адреса 0x40015000, была выделена
динамическим компоновщиком.
В этой системе библиотека С находится в файле /lib/libc-2.3.2.so. Сегмент
текста, сегмент данных и bss-сегмент библиотеки С отображены в следующие
две области, начиная с адреса 0x4002f000. Вспомним, что страничные кадры,
хранящиеся в закрытых областях, могут быть использованы несколькими
процессами через механизм "копирование при записи" при условии, что они
не изменяются. Таким образом, поскольку сегмент текста доступен только
для чтения, страничные кадры, содержащие исполняемый код из библиоте-
библиотеки С, используются совместно почти всеми процессами, работающими в этот
момент (всеми, кроме скомпонованных статически). Анонимная область па-
памяти, начинающаяся с адреса 0х4015Ь000, была выделена библиотекой С.
Анонимная область с адреса Oxbffebooo по адрес Охсооооооо ассоциирована со
стеком режима пользователя. В главе 9 мы уже описали, как стек автомати-
автоматически расширяется в сторону понижения адресов, когда это необходимо.
Последняя, одностраничная анонимная область памяти с адреса Oxffffeooo
содержит страницу vsyscall процесса, которая используется при выполнении
системного вызова и возвращении управления от обработчика сигнала (см.
главу 11).
Теперь выполним ту же программу, явно задав ограничение на размер стека
режима пользователя:
# ulimit -s 100; /tmp/memorylayout
08048000-08049000 r-xp 00000000 03:03 5042408 /tmp/memorylayout
08049000-0804a000 rwxp 00000000 03:03 5042408 /tmp/memorylayout
0804a000-08051000 rwxp 0804a000 00:00 0
b7ea3000-b7fcb000 r-xp 00000000 03:03 620804 /lib/libc-2.3.2.so
b7fcb000-b7fcf000 rwxp 00128000 03:03 620804 /lib/libc-2.3.2.so
b7fcf000-b7fd2000 rwxp b7fcf000 00:00 0
b7feb000-b7fec000 rwxp b7feb000 00:00 0
b7fec000-b8000000 r-xp 00000000 03:03 620801 /lib/ld-2.3.2.so
Ь8000000-Ь8001000 rwxp 00013000 03:03 620801 /lib/ld-2.3.2.so
bffeb000-c0000000 rwxp bffebOOO 00:00 0
ffffeOOO-fffffOOO p 00000000 00:00 0
Обратите внимание, как изменилась схема: динамический компоновщик ото-
отображен приблизительно на 128 Мбайт выше самого верхнего адреса стека.
Кроме того, поскольку области памяти библиотеки С были созданы позже,
они получили более низкие линейные адреса.
Отслеживание выполнения
Отслеживание выполнения — это методика, позволяющая одной программе
вести мониторинг выполнения другой программы. Отслеживаемая программа
может выполняться в пошаговом режиме, пока не будет принят сигнал или
пока не будет сделан системный вызов. Отслеживание выполнения широко
применяется отладчиками наряду с другими техническими приемами, такими
как задание точек останова в отлаживаемой программе и доступ к ее пере-
переменным на этапе выполнения. В этом разделе мы уделим внимание тому, как
ядро поддерживает отслеживание выполнения, и не станем обсуждать работу
отладчиков.
В Linux отслеживание выполнения осуществляется с помощью системного
вызова ptraceo, который может обрабатывать команды, перечисленные в
табл. 20.5. Процессам, у которых установлен флаг capsysptrace, разрешено
отслеживать любой процесс в системе, кроме init. И наоборот, процессу Р,
лишенному способности capsysptrace, разрешено отслеживать только про-
процессы, имеющие того же владельца, что и Р. Кроме того, за процессом не мо-
могут следить два процесса одновременно.
Таблица 20.5. Команды системного вызова ptrace в архитектуре 80*86
Команда Описание
ptrace_attach Начать отслеживание выполнения другого процесса
ptracecont Возобновить выполнение
ptracedetach Прекратить отслеживание выполнения
ptrace_get_thread_area Получить область TLS (Thread Local Storage, Локальная
память потока) от имени отслеживаемого процесса
ptracegeteventmsg Получить дополнительные данные отслеживаемого про-
процесса (например, идентификатор нового ответвленного
процесса)
ptracegetfpregs Прочитать регистры операций с плавающей точкой
ptracegetfpxregs Прочитать регистры ММХ и ХММ
ptrace_getregs Прочитать привилегированные регистры центрального
процессора
ptrace_getsiginfo Получить информацию о последнем сигнале, посланному
отслеживаемому процессу
ptrace_kill Уничтожить отслеживаемый процесс
ptraceoldsetoptions Команда, специфичная для архитектуры и эквивалентная
команде ptrace_setoptions
ptrace_peekdata Прочитать 32-битовое значение из сегмента данных
ptrace_peektext Прочитать 32-битовое значение из сегмента текста
ptracepeekusr Прочитать обычные и отладочные регистры центрального
процессора
ptracepokedata Записать 32-битовое значение в сегмент данных
ptrace_poketext Записать 32-битовое значение в сегмент текста
ptracepokeusr Записать значение в обычные и отладочные регистры
центрального процессора
ptrace_set_thread_area Установить область TLS (Thread Local Storage, Локальная
память потока) от имени отслеживаемого процесса
ptracesetfpregs Записать значение в регистры операций с плавающей
точкой
ptrace_setfpxregs Записать значение в регистры ММХ и ХММ
ptracesetoptions Модифицировать поведение системного вызова ptrace ()
ptracesetregs Записать значение в привилегированные регистры
ptrace_setsiginfo Сфабриковать информацию о последнем сигнале, послан-
посланному отслеживаемому процессу
ptracesinglestep Выполнить одну инструкцию ассемблера
Таблица 20.5 (окончание)
Команда Описание
ptrace_syscall Возобновить выполнение до следующей границы систем-
системного вызова
ptrace_traceme Начать отслеживание выполнения текущего процесса
Системный вызов ptrace () модифицирует поле parent дескриптора отслежи-
отслеживаемого процесса так, чтобы оно указывало на отслеживающий процесс. Та-
Таким образом, последний становится фактически родителем первого. Когда
отслеживание выполнения завершается, т. е. когда системный вызов ptrace ()
сделан с командой ptracedetach, он записывает в поле parent значение из
поля reaiparent, восстанавливая тем самым оригинального родителя отсле-
отслеживаемого процесса (см. главу 3).
С отслеживаемой программой можно связать несколько событий, подлежа-
подлежащих мониторингу:
П конец выполнения одной инструкции ассемблера;
□ начало системного вызова;
□ конец системного вызова;
□ прием сигнала.
Когда происходит одно из этих событий, отслеживаемая программа останав-
останавливается и ее родителю посылается сигнал sigchld. Когда родитель пожелает
возобновить выполнение потомка, он может воспользоваться одной из ко-
команд PTRACE_CONT, PTRACE_SINGLESTEP ИЛИ PTRACE_SYSCALL, В ЗаВИСИМОСТИ ОТ
события, которое ему нужно отслеживать.
Команда ptracecont просто возобновляет выполнение. Потомок выполняет-
выполняется, пока не примет еще один сигнал. Такой вид отслеживания реализован с
помощью флага ptptraced в поле ptrace дескриптора процесса. Флаг прове-
проверяется функцией dosignai () (см. главу 11).
Команда ptrace_s ingle step заставляет процесс-потомок выполнить следую-
следующую инструкцию ассемблера и снова остановиться. На машинах с архитекту-
архитектурой 80x86 этот вид отслеживания реализован с помощью флага ловушки tf в
регистре ef lags. Когда флаг установлен, после каждой инструкции ассембле-
ассемблера возбуждается исключение "Debug" (Отладка). Обработчик этого исключе-
исключения просто сбрасывает флаг, останавливает текущий процесс и посылает его
родителю сигнал sigchld. Обратите внимание, что установка флага tf не яв-
является привилегированной операцией, и процессы режима пользователя мо-
могут принудительно включить пошаговый режим выполнения даже без помо-
помощи системного вызова ptrace (). Ядро проверяет флаг ptdtrace у дескрипто-
pa процесса, чтобы узнать, был ли установлен пошаговый режим для процес-
процесса-потомка при посредстве системного вызова ptrace ().
Команда ptracesyscall заставляет отслеживаемый процесс возобновить вы-
выполнение, пока не будет сделан системный вызов. Процесс останавливается
дважды: когда системный вызов начинает работу и когда он ее заканчивает.
Этот вид отслеживания реализован при помощи флага tifsyscalltrace в
поле flags структуры threadinf о данного процесса. Флаг проверяет ассемб-
ассемблерной функцией system_caii () (см. главу 10).
Выполнение процесса можно также отследить, пользуясь отладочными сред-
средствами процессоров Intel Pentium. Например, процесс-родитель может уста-
установить значения отладочных регистров drO,..., dr7 для потомка с помощью
команды ptracepokeusr. Когда произойдет событие, отслеживаемое отла-
отладочным регистром, процессор возбудит исключение "Debug". Обработчик
исключения сможет приостановить отслеживаемый процесс и отправить ро-
родителю сигнал sigchld.
Форматы исполняемых файлов
Стандартный формат исполняемых файлов в Linux называется ELF
(Executable and Linking Format, Исполняемый и компонуемый формат). Он
был разработан в Unix System Laboratories и сейчас наиболее широко распро-
распространен в мире Unix. Некоторые известные системы Unix, например, System
V Release 4 и Solaris 2 фирмы Sun, используют ELF в качестве основного
формата.
В ранних версиях Linux поддерживался другой формат, Assembler OUTput
Format (a.out), причем фактически существовало несколько его версий. Сей-
Сейчас он встречается редко, потому что ELF является более практичным.
Операционная система Linux поддерживает и много других форматов испол-
исполняемых файлов. В результате она может выполнять программы, скомпилиро-
скомпилированные под другие операционные системы. В качестве примеров приведем
ЕХЕ-программы из MS-DOS и выполняемые файлы COFF из BSD Unix. Не-
Некоторые форматы исполняемых файлов, такие как Java или скрипты bash,
являются платформеннонезависимыми.
Формат исполняемого файла описывается объектом типа linuxbinfmt, пре-
предоставляющим три метода:
□ loadbinary — устанавливает новое окружение выполнения текущего про-
процесса, читая информацию из исполняемого файла;
П loadshiib— динамически компонует совместно используемую библио-
библиотеку с уже работающим процессом; активизируется системным вызовом
uselib ();
□ coredump — сохраняет контекст выполнения текущего процесса в файле с
именем core. Этот файл, формат которого зависит от типа исполняемого
файла программы, обычно создается, когда процесс получает сигнал, по
умолчанию выполняющий действие "дамп" (см. главу 11).
Все объекты linuxbinfmt занесены в однонаправленный список, а адрес его
первого элемента хранится в переменной formats. Элементы добавляются в
СПИСОК И удаляются ИЗ него функциями registerJoinfmt () И unregister_
binfmt (). Функция registerbinfmt () выполняется при запуске системы для
каждого формата исполняемого файла, вкомпилированного в ядро. Эта функ-
функция выполняется также при загрузке модуля, реализующего новый формат
исполняемых файлов. Когда модуль выгружается, выполняется функция
unregister_binfmt ().
Последним элементом в списке formats всегда является объект, описываю-
описывающий формат интерпретируемых скриптов. Этот формат определяет только
метод loadbinary. Соответствующая ему функция loadscript () проверяет,
начинается ли исполняемый файл с пары символов #!. Если это так, она вос-
воспринимает остальные символы первой строчки как путь к другому исполняе-
исполняемому файлу и пытается выполнить его, передав в качестве параметра имя
файла со скриптом5.
Linux разрешает пользователям регистрировать собственные форматы ис-
исполняемых файлов. Каждый такой формат распознается либо благодаря ма-
магическому числу, занимающему первые 128 байтов файла, либо по расшире-
расширению имени файла. Например, в MS-DOS расширение состоит из трех симво-
символов, отделенных от имени точкой: .ехе идентифицирует выполняемые
программы, a .bat — скрипты оболочки.
Когда ядро определяет, что исполняемый файл имеет пользовательский фор-
формат, оно запускает соответствующую программу-интерпретатор. Программа-
интерпретатор работает в режиме пользователя, принимает путь к испол-
исполняемому файлу в качестве параметра и выполняет вычисления. Например,
исполняемый файл с программой на языке Java обрабатывается виртуальной
машиной Java, которая может называться /usr/lib/java/bin/java.
Этот механизм аналогичен формату скриптов, но он мощнее, т. к. не накла-
накладывает на пользовательский формат никаких ограничений. Для регистрации
нового формата пользователь записывает в файл register специальной файло-
5 Выполнить файл скрипта можно, даже если он не начинается с символов # !, если он написан на
языке, распознаваемом командной оболочкой. Однако в этом случае скрипт интерпретируется либо
оболочкой, в которой пользователь ввел команду, либо оболочкой Bourne (sh), то есть ядро не уча-
участвует в этом напрямую.
вой системы binfmtmisc (обычно монтируемой на каталоге /proc/sys/fs
/binfmtmisc) строку следующего вида:
:name:type:offset:string:mask:interpreter:flags
Каждое поле имеет определенный смысл:
□ name — идентификатор нового формата;
□ type — способ распознавания (м — магическое число, е — расширение);
□ offset — начальное смещение магического числа внутри файла;
□ string — последовательность байтов, которая должна совпасть либо с ма-
магическим числом, либо с расширением файла;
□ mask — строка для маскировки некоторых битов в string;
□ interpreter — полный путь к программе-интерпретатору;
□ flags — необязательные флаги, управляющие способом вызова програм-
программы-интерпретатора.
Например, следующая команда, выполненная суперпользователем, позволяет
ядру распознавать выполняемый формат Microsoft Windows:
$ echo f:DOSWin:M:0:MZ:0xff:/usr/bin/wine:'
>/proc/sys/fs/binfmt misc/register
Формат исполняемых файлов Windows содержит в первых двух байтах маги-
магическое число MZ и поддерживается программой-интерпретатором
/usr/bin/wine.
Области выполнения
Как было сказано в главе 7, приятной особенностью операционной системы
Linux является ее умение выполнять файлы, откомпилированные для других
операционных систем. Конечно, это возможно, лишь когда файлы содержат
машинный код той компьютерной архитектуры, на которой работает ядро.
Таким "чужеродным" программам предоставляется поддержка двух типов:
□ эмулированное выполнение: необходимо для программ, содержащих сис-
системные вызовы, не удовлетворяющие стандарту POSIX;
□ реальное выполнение: годится для программ, у которых системные вызо-
вызовы полностью удовлетворяют стандарту POSIX.
Программы Microsoft MS-DOS и Windows эмулируются. Их нельзя выпол-
выполнить реально, потому что они содержат интерфейсы API, не распознаваемые
операционной системой Linux. Эмулятор, например DOSemu или Wine, вы-
зывается для преобразования каждого обращения к API в вызов эмулирую-
эмулирующей интерфейсной функции, которая, в свою очередь, делает необходимые
системные вызовы Linux. Поскольку эмуляторы реализованы, в основном,
как приложения режима пользователя, они здесь более не обсуждаются.
Зато программы, удовлетворяющие стандарту POSIX и откомпилированные в
операционных системах, отличных от Linux, могут быть выполнены без про-
проблем, потому что POSIX-системы имеют схожие API-интерфейсы. (На самом
деле, API-интерфейсы должны быть идентичны, но это не всегда так.) Незна-
Незначительные расхождения, "сглаживание" которых входит в обязанности ядра,
обычно относятся к тому, как делаются системные вызовы, или как нумеру-
нумеруются отдельные сигналы. Эта информация хранится в дескрипторах облас-
областей выполнения, ИМеЮЩИХ ТИП exec_domain.
Процесс задает свою область выполнения, устанавливая поле personality
своего дескриптора и сохраняя адрес соответствующей структуры
exec_domain В поле exec_domain структуры thread_info. Процесс может ИЗМе-
нить поле personality при помощи системного вызова personality о . Типич-
ные значения параметров этого системного вызова перечислены в табл. 20.6.
Предполагается, что программисты не будут напрямую менять эти поля
у своих программ; наоборот, системный вызов personality о должен быть
сделан в коде, устанавливающем контекст выполнения процесса (см. сле-
следующий раздел).
Таблица 20.6. Виды процессов, поддерживаемые ядром Linux
Вид Операционная система
per_linux Стандартная область выполнения
per_linux_32BIT Linux с 32-битовыми физическими адресами в 64-битовых
архитектурах
per_linux_fdpic Программа Linux в формате ELF FDPIC
per_svr4 System V Release 4
per_svr3 System V Release 3
PERSCOSVR3 SCO Unix Version 3.2
PEROSR5 SCO OpenServer Release 5
per_wysev386 Unix System V/386 Release 3.2.1
per_iscr4 Interactive Unix
perbsd BSD Unix
PER_SUNOS SunOS
per_xenix Xenix
Таблица 20.6 (окончание)
Вид Операционная система
per_linux32 Эмуляция 32-битовых программ Linux в 64-битовых архитектурах
(с использованием адресного пространства режима пользователя
в 4 Гбайт)
per_linux32_3GB Эмуляция 32-битовых программ Linux в 64-битовых архитектурах
(с использованием адресного пространства режима пользователя
в 3 Гбайт)
PERIRIX32 SGI IRIX-5 32-битовая
PERIRIXN32 SGI IRIX-6 32-битовая
PERIRIX64 SGI IRIX-6 64-битовая
PER_RISCOS RISC OS
per_solaris Solaris фирмы Sun
per_uw7 UnixWare 7 фирмы SCO (бывшая Caldera)
PEROSF4 Digital UNIX (Compaq Tru64 UNIX)
per_hpux HP-UX фирмы Hewlett-Packard
Функции exec
Системы Unix предоставляют семейство функций, заменяющих контекст вы-
выполнения процесса на новый контекст, описанный в исполняемом файле.
Имена этих функций имеют префикс exec, за которым следует одна или две
буквы, и поэтому на неспецифичного представителя этого семейства обычно
ссылаются как на функцию exec.
Функции exec перечислены в табл. 20.7. Они различаются способами интер-
интерпретации параметров.
Таблица 20.7. Функции exec
Поиск Аргументы команд- Массив око„жеНия
в переменной PATH ной строки Массив окружения
execl () Нет Список Нет
execlpO Да Список Нет
execle () Нет Список Да
execv() Нет Массив Нет
execvpO Да Массив Нет
execveO Нет Массив Да
Первый параметр каждой функции обозначает путь к файлу; который должен
быть выполнен. Путь может быть абсолютным или задан относительно те-
текущего каталога процесса. Кроме того, если путь не содержит символов "/",
функции execipo и execvp () ищут исполняемые файлы во всех каталогах,
заданных в переменной среды path.
Кроме первого параметра, функции execi (), execip () и execie () принимают
переменное количество дополнительных параметров. Каждый из них указы-
указывает на строку, описывающую аргумент командной строки для новой про-
программы. Как можно догадаться по букве "i" в именах этих функций, парамет-
параметры организованы в список, заканчивающийся значением null. Обычно пер-
первый аргумент командной строки совпадает с именем исполняемого файла.
Что касается функций execv (), execvp () и execve (), они задают аргументы
командной строки с помощью одного параметра. Буква 'Vм в именах этих
функций подсказывает, что параметр является адресом вектора указателей на
строки, описывающие аргументы командной строки. Последний элемент
массива должен содержать null.
Функции execie о и execve о в качестве последнего параметра принимают
адрес массива указателей на строки окружения. Как всегда, последний эле-
элемент массива должен содержать null. Другие функции могут получить дос-
доступ к окружению новой программы через внешнюю глобальную переменную
environ , которая определена в библиотеке С.
Все функции exec, кроме execve (), являются интерфейсными. Они определе-
определены в библиотеке С и обращаются к функции execve (), которая представляет
собой единственный системный вызов, предлагаемый операционной систе-
системой Linux для управления выполнением программы.
Служебная процедура sysexecve () принимает следующие параметры:
□ адрес пути к исполняемому файлу (в адресном пространстве режима поль-
пользователя);
□ адрес массива (в адресном пространстве режима пользователя) указателей
на строки (и опять, в адресном пространстве режима пользователя); каж-
каждая строка представляет аргумент командной строки, а последний элемент
массива содержит null;
□I адрес массива (в адресном пространстве режима пользователя) указателей
на строки (и опять, в адресном пространстве режима пользователя); каж-
каждая строка представляет переменную окружения в формате имя=значение,
а последний элемент массива содержит null.
Функция копирует путь к исполняемому файлу в новый выделенный стра-
страничный кадр. Затем она вызывает функцию doexecve (), передавая ей указа-
указатели на страничный кадр, на массивы указателей и на то место в стеке режи-
режима ядра, где сохранено содержимое регистров режима пользователя.
Что касается функции doexecve () , она выполняет следующие действия:
1. Динамически выделяет структуру linuxbinprm, которая будет заполнена
информацией о новом исполняемом файле.
2. Вызывает функции path_lookup(), dentry_open () И path_release (), чтобы
получить три объекта, ассоциированные с исполняемым флагом: элемент
каталога, файловый объект и индексный дескриптор. В случае неуспеха
функция возвращает соответствующий код ошибки.
3. Убеждается, что файл может быть выполнен текущим процессом. Кроме
того, функция убеждается, что в файл не производится запись, для чего
она проверяет поле iwritecount индексного дескриптора и записывает
туда -1, чтобы запретить операции записи в будущем.
4. В МНОГОПрОЦеССОрНЫХ Системах фуНКЦИЯ ВЫЗЫВаеТ фуНКЦИЮ sched_exec (),
чтобы найти наименее загруженный процессор, который мог бы выпол-
выполнить новую программу, и переносит на него ему текущий процесс (см.
главу 7).
5. Вызывает функцию initnewcontext (), чтобы проверить, пользовался ли
текущий процесс собственной локальной таблицей дескрипторов (см. гла-
главу 2). Если пользовался, функция выделяет и заполняет новую локальную
таблицу дескрипторов для новой программы.
6. Вызывает фуНКЦИЮ prepare_binprm(), ЧТОбы ЗаПОЛНИТЬ Структуру linux_
binprm. Co своей стороны, вызванная функция выполняет следующие дей-
действия:
• снова проверяет, может ли файл быть выполнен (то есть имеется ли у
кого-нибудь право на его выполнение). Если нет, возвращает код
ошибки. (Проверки на шаге 3 недостаточно, т. к. процесс с установлен-
установленным флагом capdacoverride всегда удовлетворяет условиям проверки);
• Инициализирует ПОЛЯ e_uid И egid Структуры linuxbinpm, Принимая
во внимание состояние флагов setuid и setgid выполняемого файла.
Эти поля содержат эффективные идентификаторы пользователя и
группы соответственно. Кроме того, функция проверяет способности
процесса;
• записывает в поле buf структуры linuxbinprm первые 128 байтов ис-
исполняемого файла. В этих байтах находится магическое число формата
исполняемого файла и прочая информация, необходимая для распозна-
распознавания исполняемого файла.
7. Копирует путь к файлу, аргументы командной строки и строки окружения
в один или несколько новых выделенных страничных кадров. (В конечном
счете, все они присваиваются адресному пространству режима пользова-
пользователя.)
8. Вызывает функцию searchbinaryhandier о, которая перебирает элемен-
элементы списка formats и пытается вызвать у каждого метод loadbinary, пере-
передавая ему структуру linux_binprm. Перебор элементов списка formats за-
заканчивается, как только какой-нибудь метод loadbinary распознает
формат исполняемого файла.
9. Если формат файла отсутствует в списке formats, функция освобождает
все выделенные страничные кадры и возвращает код ошибки -enoexec.
Система Linux не в состоянии распознать формат этого файла.
10. В противном случае функция освобождает структуру linuxbinprm и воз-
возвращает код, полученный от метода loadbinary, ассоциированного с
форматом исполняемого файла.
Метод loadbinary, соответствующий формату исполняемого файла, дейст-
действует следующим образом (мы предполагаем, что исполняемый файл хранится
в файловой системе, допускающей его отображение в память, и что ему тре-
требуется одна или несколько совместно используемых библиотек):
1. Проверяет магические числа из первых 128 байтов файла, чтобы иденти-
идентифицировать формат. Если идентификация не удается, возвращает код
ОШИбкИ -ENOEXEC.
2. Читает заголовок исполняемого файла. Заголовок описывает сегменты
программы и необходимые разделяемые библиотеки.
3. Получает от исполняемого файла путь к динамическому компоновщику,
который найдет совместно используемые библиотеки и отобразит их в па-
память.
4. Получает объект "элемент каталога" (а также объект "индексный дескрип-
дескриптор" и файловый объект) динамического компоновщика.
5. Проверяет у динамического компоновщика право на выполнение.
6. Копирует первые 128 байтов динамического компоновщика в буфер.
7. Выполняет некоторые проверки на непротиворечивость типа динамиче-
динамического компоновщика.
8. Вызывает функцию f lushoidexec (), чтобы освободить почти все ресур-
ресурсы, использованные в предыдущих вычислениях. Вызванная функция вы-
выполняет следующие действия:
• если таблица обработчиков сигналов используется совместно с други-
другими процессами, функция выделяет новую таблицу и уменьшает счетчик
обращений у старой. Кроме того, функция отсоединяет процесс от
прежней группы потоков (см. главу 3). Все это делается с помощью
функции de_thread ();
• вызывает функцию uns-hare_fiies(), чтобы сделать копию структуры
fiiesstruct, содержащей открытые файлы процесса, если она ис-
используется совместно с другими процессами (см. разд. "Фаты, свя-
связанные с процессом4 главы 12);
• вызывает функцию execmmap (), чтобы освободить дескриптор памяти,
все области памяти и все страничные кадры, выделенные процессу, и
почистить Таблицу Страниц процесса;
• записывает в поле comm дескриптора процесса путь к исполняемому
файлу;
• вызывает функцию f lushthread (), чтобы очистить регистры опера-
операций с плавающей точкой и отладочные регистры, сохраненные в сег-
сегменте TSS. Обновляет таблицу обработчиков сигналов, устанавливая
для каждого сигнала действие по умолчанию. Это делается с помощью
функции f lush_signal_handlers ();
• вызывает функцию fiushoidfiies о, чтобы закрыть все открытые
файлы, у которых установлен соответствующий флаг в поле fiies->
closeonexec дескриптора процесса (см. разд. "Файлы, связанные с
процессом" главы 12f.
Теперь мы дошли до точки, из которой нет возврата: функция не может
восстановить предыдущие вычисления, если что-то пойдет не так.
9. Сбрасывает флаг pfforknoexec у дескриптора процесса. Этот флаг, уста-
устанавливаемый при ответвлении процесса и сбрасываемый при выполнении
новой программы, необходим для учета процессов.
10. Записывает новое значение в поле personality дескриптора процесса.
И. Вызывает функцию archpickramapiayout (), чтобы выбрать схему рас-
расположения областей памяти для процесса.
12. Вызывает функцию setupargpages (), чтобы выделить новый дескрип-
тор области памяти для стека режима пользователя, принадлежащего
процессу, и занести эту область памяти в адресное пространство процес-
процесса. Кроме того, функция setup_arg_pages() присваивает новой области
памяти страничные кадры, содержащие строки с переменными окруже-
окружения и аргументы командной строки.
13. Вызывает функцию dommapO, чтобы создать новую область памяти, ко-
которая отобразит сегмент текста (то есть код) исполняемого файла. На-
Начальный линейный адрес этой области памяти зависит от формата испол-
исполняемого файла, потому что код программы, как правило, не является пе-
перемещаемым. Таким образом, функция предполагает, что сегмент текста
6 Эти флаги могут быть прочитаны и изменены с помощью системного вызова f cntl ().
загружается с учетом определенного смещения логического адреса
(и, следовательно, начиная с определенного линейного адреса). Програм-
Программы ELF загружаются, начиная с линейного адреса 0x08048000.
14. Вызывает функцию dommapo, чтобы создать новую область памяти, ко-
которая отобразит сегмент данных исполняемого файла. И опять начальный
линейный адрес этой области памяти зависит от формата исполняемого
файла, потому что код ожидает найти свои переменные по определенным
смещениям (то есть по определенным линейным адресам). У программы
ELF сегмент данных загружается сразу после сегмента текста.
15. Выделяет дополнительные области памяти для всех остальных специали-
специализированных сегментов исполняемого файла. Обычно таковые отсутст-
отсутствуют.
16. Вызывает функцию, загружающую динамический компоновщик. Если он
имеет формат ELF, функция называется loadeifinterp (). Вообще гово-
говоря, эта функция выполняет шаги с 12 по 14, но для динамического компо-
компоновщика, а не файла, которому он потребовался. Начальные адреса об-
областей памяти, предназначенных для текста и данных динамического
компоновщика, указываются самим компоновщиком. Впрочем, они рас-
расположены достаточно высоко (обычно выше Ох4ооооооо), чтобы избежать
конфликтов с областями памяти, отображающими текст и данные файла,
подлежащего выполнению.
17. Записывает в поле binfmt дескриптора процесса адрес объекта
linuxbinfmt формата исполняемого файла.
18. Определяет новые способности процесса.
19. Создает специфические таблицы динамического компоновщика и сохра-
сохраняет их в стеке режима пользователя между аргументами командной
строки и массивов указателей на строки окружения (см. рис. 20.1).
20. Записывает необходимые значения в поля startcode, endcode,
start_data, end_data, start_brk, brk И start_stack дескриптора области
памяти процесса.
21. Вызывает функцию dobrko, чтобы создать новую анонимную область
памяти, отображающую bss-сегмент программы. (Когда процесс записы-
записывает значение в переменную, срабатывает механизм выделения страниц
по требованию, в результате чего выделяется страничный кадр.) Размер
этой области памяти вычисляется при компоновке исполняемой про-
программы. Начальный линейный адрес области памяти должен быть задан,
поскольку код программы, как правило, не является перемещаемым.
У программ ELF сегмент bss загружается сразу после сегмента данных.
22. Вызывает макрос startthreado, чтобы изменить значения регистров
режима пользователя eip и esp (сохраненных в стеке режима ядра) так,
чтобы они указывали на точку входа динамического компоновщика и на
верхушку нового стека режима пользователя соответственно.
23. Если выполнение процесса отслеживается, функция уведомляет отладчик
о завершении системного вызова execve ().
24. Возвращает 0 (успех).
Когда системный вызов execve о завершается, и вызвавший процесс возоб-
возобновляет свое выполнение в режиме пользователя, контекст выполнения рез-
резко меняется: кода, сделавшего системный вызов, больше не существует.
В этом смысле мы могли бы сказать, что системный вызов execve () никогда
не завершается успешно. Зато новая программа, подлежащая выполнению,
оказывается отображенной в адресное пространство процесса.
Однако выполнять ее еще нельзя, потому что динамический компоновщик
должен позаботиться о загрузке совместно используемых библиотек7.
Хотя динамический компоновщик работает в режиме пользователя, мы
вкратце опишем его действия. Его первая задача— установить базовый кон-
контекст выполнения для самого себя, начав с информации, сохраненной ядром в
стеке режима пользователя между массивом указателей на строки окружения
и полем argstart. Затем динамический компоновщик должен изучить про-
программу, подлежащую выполнению, на предмет того, какие совместно исполь-
используемые библиотеки должны быть загружены и какие функции в каждой из
них будут вызваны. Затем интерпретатор делает несколько системных вызо-
вызовов mmap (), чтобы создать области памяти, отображающие страницы, которые
будут содержать библиотечные функции (текст и данные), фактически необ-
необходимые программе. После этого интерпретатор обновляет все ссылки на
символы совместно используемой библиотеки в соответствии с линейными
адресами областей памяти этой библиотеки. Наконец, динамический компо-
компоновщик завершает свое выполнение, передав управление в главную точку
входа программы, подлежащей выполнению. С этого момента процесс будет
выполнять код файла и совместно используемых библиотек.
Вы, должно быть, уже заметили, что выполнение программы является до-
довольно сложным видом деятельности, в которой участвуют многие составные
части конструкции ядра: абстракция процессов, управление памятью, сис-
системные вызовы и файловые системы. Изучая эту тему, начинаешь понимать,
каким изумительным созданием человеческого ума является Linux!
7 Все гораздо проще, если исполняемый файл скомпонован статически, то есть не нуждается в со-
совместно используемых библиотеках. Метод loadbinary просто отображает сегменты программы
(текст, данные, bss и стек) в области памяти процесса, а затем записывает в регистр eip режима
пользователя точку входа новой программы.
ПРИЛОЖЕНИЕ 1
Запуск системы
В этом приложении описано, что происходит непосредственно после вклю-
включения компьютера: как образ ядра Linux копируется в память и затем выпол-
выполняется. Короче, мы обсудим, как загружается ядро и, следовательно, вся сис-
система.
Слово "bootstrap", обозначающее эту процедуру в английском языке, изна-
изначально относилось к человеку, который обувается и встает. Применительно к
операционным системам, этот термин обозначает запись в память хотя бы
небольшой порции операционной системы и ее выполнение процессором.
Кроме того, он обозначает инициализацию структур ядра, создание несколь-
нескольких пользовательских процессов и передачу управления одному из них.
Загрузка компьютерной системы является долгим и скучным делом, потому
что изначально почти все аппаратные устройства, включая оперативную па-
память, находятся в случайном, непредсказуемом состоянии. Более того, про-
процесс загрузки сильно зависит от архитектуры компьютера. Как и раньше в
этой книге, мы будем говорить об архитектуре 80x86.
Доисторические времена: BIOS
Сразу после своего включения компьютер практически бесполезен, потому
что в ячейках памяти находятся случайные данные, и никакая операционная
система не работает. Чтобы начать загрузку, специальная электронная схема
устанавливает логическое значение на контакте RESET центрального процес-
процессора. После установки RESET некоторые регистры процессора (в том числе
cs и eip) принимают определенные значения, и выполняется код, находящий-
находящийся по физическому адресу OxfffffffO. Этот адрес хранится в специальной
микросхеме постоянной памяти, предназначенной только для чтения. Ее час-
часто называют постоянным запоминающим устройством (ПЗУ). Набор про-
грамм, хранящихся в ПЗУ, в архитектуре 80x86 традиционно называется
BIOS (Basic Input/Output System, Базовая система ввода/вывода), так как в
этот набор входит несколько низкоуровневых процедур, управляемых преры-
прерываниями и используемых всеми операционными системами на этапе загрузки
для работы с устройствами, из которых состоит компьютер. В некоторых
операционных системах, например Microsoft MS-DOS, с помощью BIOS реа-
реализовано большинство системных вызовов.
Находясь в защищенном режиме (см. главу 2), Linux больше не пользуется
BIOS и устанавливает собственные драйверы для всех аппаратных устройств
компьютера. На самом деле, процедуры BIOS должны выполняться в реаль-
реальном режиме, так что они не могут иметь общих функций с Linux, даже если
это было бы выгодно.
BIOS использует адреса реального режима, потому что только они доступны
после включения компьютера. Адрес реального режима состоит из сегмента
seg и смещения off; соответствующий физический адрес задается формулой
segxl6+off. В результате схеме адресации центрального процессора для пре-
преобразования логического адреса в физический не нужны ни глобальная таб-
таблица дескрипторов, ни локальная таблица дескрипторов, ни таблица страниц.
Очевидно, что код, инициализирующий эти таблицы, должен работать в ре-
реальном режиме.
Операционная система Linux вынуждена пользоваться BIOS на этапе загруз-
загрузки, когда ей нужно прочитать образ ядра с диска или иного внешнего устрой-
устройства. Процедура BIOS, осуществляющая начальную загрузку, выполняет
следующие действия:
1. Проводит серию тестов аппаратной части компьютера, чтобы выяснить,
какие устройства имеются в наличии, и исправны ли они. Этот этап часто
называется POST (Power-On Self-Test, Самотестирование после включения
питания). По ходу этого этапа на экран выводится несколько сообщений, в
том числе и логотип BIOS с номером версии.
В современных компьютерах 80^86, AMD64 и Itanium используется
стандарт ACPI (Advanced Configuration and Power Interface, Расширенный
интерфейс настройки и питания). Код для начальной загрузки в ACPI-
совместимой BIOS строит несколько таблиц, которые описывают аппарат-
аппаратные устройства, присутствующие в системе. Эти таблицы имеют формат,
не зависящий от производителя, и ядро операционной системы может чи-
читать их, чтобы узнать, как обращаться с устройствами.
2. Инициализирует аппаратные устройства. Этот этап является принципи-
принципиальным для современных PCI-архитектур, поскольку он гарантирует, что
все устройства будут функционировать без конфликтов на IRQ-линиях и
портах ввода/вывода. По окончании этапа на дисплей выводится таблица
установленных РС1-устройств.
3. Выполняет поиск операционной системы, которую следует загрузить.
В зависимости от настроек BIOS, процедура пытается обратиться (в опре-
определенном порядке) к первому сектору (загрузочному сектору) каждого
гибкого диска, жесткого диска и CD-ROM в системе.
4. Как только обнаруживается нужное устройство, процедура копирует со-
содержимое его первого сектора в оперативную память, начиная с физиче-
физического адреса 0х00007с00, а затем переходит по этому адресу и выполняет
только что загруженный код.
Далее в этом приложении описывается переход от "первобытного" состояния
к работающей системе Linux во всей ее красе.
Античность: загрузчик
Загрузчик — это программа, которую вызывает процедуры BIOS, чтобы за-
загрузить образ операционной системы в оперативную память. Мы вкратце
объясним работу загрузчиков в архитектуре IBM PC.
При загрузке с дискеты инструкции, хранящиеся в первом секторе, загружа-
загружаются в оперативную память и выполняются. Эти инструкции копируют в па-
память все остальные секторы с образом ядра.
Загрузка с жесткого диска происходит иначе. Первый сектор жесткого диска,
называемый MBR (Master Boot Record, Главная загрузочная запись), содер-
содержит таблицу разделов1 и небольшую программу, которая загружает первый
сектор раздела, содержащего запускаемую операционную систему. В некото-
некоторых операционных системах, например Microsoft Windows 98, этот раздел
идентифицируется флагом active в таблице раздела2. Согласно этому подхо-
подходу, могут быть загружены только операционные системы, у которых образ
ядра находится в активном разделе. Как мы скоро убедимся, Linux ведет себя
более гибко, потому что заменяет рудиментарную программу, хранящуюся в
MBR, на более сложную— загрузчик, позволяющий пользователям выби-
выбирать, какую операционную систему следует загрузить.
Образы ядра старых версий Linux (вплоть до 2.4) содержали минимальный
загрузчик в первых 512 байтах. В результате копирование образа ядра, начи-
начиная с первого сектора, автоматически делало дискету загружаемой. Образы
ядра Linux 2.6 больше не включают в себя такой загрузчик. Таким образом,
чтобы загрузить операционную систему с дискеты, нужно иметь загрузчик
1 Каждая запись в таблице разделов обычно задает первый и последний секторы раздела и тип опе-
операционной системы, которая с ним работает.
2 Флаг active может быть установлен подходящей программой, например fdisk.
в первом секторе. Теперь загрузка с дискеты очень похожа на загрузку
с жесткого диска или компакт-диска.
Загрузка Linux с диска
Для загрузки ядра Linux с диска нужен двухступенчатый загрузчик. Всем из-
известный загрузчик Linux в архитектуре 80x86 называется Linux LOader
(LILO). Существуют и другие загрузчики для систем 80x86, например, GRand
Unified Bootloader (GRUB) распространен достаточно широко. GRUB являет-
является более развитым загрузчиком, чем LILO; он распознает несколько дисковых
файловых систем и, следовательно, способен читать порции загрузчика из
файлов. Конечно, специфические загрузчики имеются для всех архитектур,
поддерживаемых системой Linux.
LILO можно установить либо в сектор MBR (заменив им маленькую про-
программу, копирующую загрузочный сектор активного раздела), либо в загру-
загрузочный сектор каждого раздела диска. В обоих случаях результат одинако-
одинаковый: когда загрузчик выполняется, пользователь может выбрать загружаемую
операционную систему.
На самом деле, загрузчик LILO слишком велик и не помещается в один сек-
сектор, поэтому он разбит на две части. MBR или загрузочный сектор раздела
содержит маленький загрузчик, который записывается системой BIOS в опе-
оперативную память, начиная с адреса 0х00007с00. Эта программа переписывает
себя по адресу 0х0009ба00, устанавливает стек реального режима (в диапазоне
адресов от 0х00098000 до 0x000969ff), загружает вторую часть LILO с адреса
0х0009бс00 и переходит по этому адресу.
Эта вторая программа читает с диска карту загружаемых операционных сис-
систем и предлагает пользователю меню (или командную строку), чтобы он мог
выбрать одну из них. Когда пользователь сделает выбор (или промедлит в
течение определенного времени, и LILO выберет систему по умолчанию),
загрузчик либо скопирует в память загрузочный сектор существующего раз-
раздела и выполнит его, либо напрямую скопирует образ ядра.
В случае копирования образа ядра Linux загрузчик LILO, использующий
в своей работе процедуры BIOS, выполняет следующие действия:
1. Вызывает процедуру BIOS, которая выводит на экран сообщение
"Loading" (Загрузка).
2. Вызывает процедуру BIOS, которая загружает с диска первую часть
образа ядра: первые 512 байтов помещаются в память, начиная с адре-
адреса 0x00090000, а код функции setup о записывается, начиная с адреса
0x00090200.
3. Вызывает процедуру BIOS, которая загружает с диска остальную часть
образа ядра и записывает ее, начиная либо с нижнего адреса Охоооюооо
(для маленьких образов, откомпилированных с помощью команды make
zimage), либо с верхнего адреса Охооюоооо (для больших образов, отком-
откомпилированных с помощью команды make bzimage). В дальнейшем мы бу-
будем говорить, что образ ядра "загружен низко" или "загружен высоко" со-
соответственно. Поддержка больших образов ядра основана, по сути, на той
же схеме загрузки, что и маленьких, но данные располагаются по другим
физическим адресам, чтобы избежать проблем с "дырой ISA", упомянутой
в главе 2.
4. Переходит на выполнение кода функции setup ().
Средние века: функция setupf)
Код ассемблерной функции setup о записан компоновщиком в файл с обра-
образом ядра со смещением 0x2 о о. Загрузчик легко находит этот код и копирует
его в оперативную память, начиная с адреса 0x00090200.
Функция setup () должна инициализировать аппаратные устройства компью-
компьютера и установить окружение для выполнения программы ядра. Хотя система
BIOS уже проинициализировала большинство аппаратных устройств, Linux
не полагается на нее и заново инициализирует устройства по-своему, чтобы
усилить переносимость и устойчивость. Функция setup о выполняет
следующие действия:
1. В системах, удовлетворяющих стандарту ACPI, она вызывает процедуру
BIOS, которая строит в оперативной памяти таблицу, описывающую схе-
схему расположения физических областей памяти в системе (эту таблицу
можно увидеть среди сообщений ядра на этапе загрузки, если поискать
метку "BIOS-e820"). В старых версиях операционной системы эта функция
вызывает процедуру BIOS, которая просто возвращает размер оператив-
оперативной памяти в системе.
2. Настраивает параметры клавиатуры. (Если пользователь держит клавишу
нажатой в течение установленного времени, клавиатура начинает снова и
снова отправлять процессору соответствующий код клавиши через уста-
установленные интервалы.)
3. Инициализирует видеоадаптер.
4. Заново инициализирует контроллер диска и определяет параметры диска.
5. Проверяет шину IBM Micro Channel (MCA).
6. Проверяет устройство PS/2 (мышь).
7. Проверяет, поддерживает ли BIOS систему управления питанием АРМ
(Advanced Power Management).
8. Если BIOS поддерживает EDD (Enhanced Disk Drive Services, Расширен-
Расширенные службы дисковых приводов), функция вызывает необходимую про-
процедуру, чтобы построить оперативной памяти таблицу, описывающую
жесткие диски, доступные в системе. Информация, содержащаяся в таб-
таблице, доступна в файлах каталога firmware/edd специальной файловой
системы sysfs.
9. Если образ ядра был загружен в память низко (начиная с физического ад-
адреса Охоооюооо), функция перемещает его по адресу Охооооюоо. Если же
образ ядра был загружен высоко, функция оставляет его на месте. Этот
шаг необходим, потому что с целью хранения образа ядра на дискете и
для ускорения загрузки образ ядра хранится на диске в упакованном виде,
а процедуре распаковки нужно свободное место в памяти вслед за обра-
образом ядра под временный буфер.
10. Настраивает вход А20 контроллера клавиатуры 8042. Вход А20 был спе-
специально создан в системах 80286, для совместимости физических адресов
с адресами старых процессоров 8088. К сожалению, настройка А20 необ-
необходима до включения защищенного режима, иначе центральный процес-
процессор будет считать, что двадцать первый бит любого физического адреса
равен нулю. Настройка А20 нарушает стройность описываемой
процедуры.
11. Устанавливает временную таблицу дескрипторов прерываний (IDT) и
временную глобальную таблицу дескрипторов (GDT).
12. Настраивает блок операций с плавающей точкой (FPU), если таковой
имеется.
13. Перепрограммирует программируемые контроллеры прерываний (PIC),
чтобы замаскировать все прерывания, кроме IRQ2, которое является кас-
каскадным прерыванием между двумя Р1С-контроллерами.
14. Переключает центральный процессор с реального режима на защищен-
защищенный, устанавливая без ре в регистре состояния его. Бит pg в регистре его
сброшен, поэтому управление страницами по-прежнему не работает.
15. Передает управление ассемблерной функции startup_32 ().
Эпоха Возрождения: функции startup_32()
Существуют две различные функции startup_32 о. Та, о которой мы будем
говорить, находится в файле arch/i386/boot/compressed/head.S. По окончании
работы функции setup о функция startup_32() находится по адресу либо
Охооюоооо, либо Охооооюоо, в зависимости от того, высоко или низко загру-
загружен образ ядра.
Эта функция выполняет следующие действия:
1. Инициализирует регистры сегментации и временный стек.
2. Сбрасывает все биты регистра еflags.
3. Заполняет нулями область неинициализированных данных ядра, иденти-
идентифицируемую символами edata и end (см. главу 2).
4. Вызывает функцию decompresskernei о для распаковки образа ядра. Вна-
Вначале появляется сообщение "Uncompressing Linux..." (Распаковка Linux).
По окончании распаковки выводится сообщение "OK, booting the kernel"
(Все в порядке, загружается ядро). Если образ ядра был загружен низко,
распакованное ядро помещается по физическому адресу Охооюоооо.
В противном случае оно помещается во временный буфер после упако-
упакованного образа. Затем распакованный образ переносится в окончательное
положение, начинающееся с физического адреса Охооюоооо.
5. Переходит по физическому адресу Охооюоооо.
Распакованный образ ядра начинается со второй функции startup_32 (), кото-
которая находится в файле arch/i386/kernel/head.S. Совпадение имен функций не
создает проблем (разве что при чтении этого раздела), потому что обе они
вызываются путем передачи управления по конкретным физическим адресам.
Вторая функция startup_32 о устанавливает среду выполнения для первого
процесса Linux (процесса 0). Она выполняет следующие действия:
1. Инициализирует регистры сегментации их окончательными значениями.
2. Заполняет нулями bss-сегмент ядра (см. разд. "Сегменты программы и об-
области памяти процесса" гл. 20).
3. Инициализирует временные Таблицы Страниц ядра, содержащиеся в
swapperpgdir и рдО, чтобы точно отобразить линейные адреса в те же са-
самые физические адреса (см. главу 2).
4. Сохраняет адрес глобального каталога страниц в регистре сгЗ и включает
управление страницами, устанавливая бит pg в регистре его.
5. Подготавливает стек режима ядра для процесса 0 (см. главу 3).
6. Еще раз сбрасывает все биты в регистре еflags.
7. Вызывает функцию setupidto для заполнения таблицы IDT "пустыми"
обработчиками прерываний (см. главу 4).
8. Записывает параметры системы, полученные от BIOS, и параметры, пере-
переданные операционной системе, в первый страничный кадр (см. главу 2).
9. Идентифицирует модель процессора.
10. Загружает в регистры gdtr и idtr адреса таблиц GDT и IDT.
11. Передает управление функции startkernei ().
Новейшее время: функция start_kernel()
Функция startkernei () завершает инициализацию ядра Linux. Она инициа-
инициализирует почти каждый компонент ядра; мы упомянем лишь некоторые
из них:
□ планировщик инициализируется с помощью функции schedinito (см.
главу 7);
П ЗОНЫ ПаМЯТИ ИНИЦИаЛИЗИруЮТСЯ С ПОМОЩЬЮ фуНКЦИИ build_all_
zoneiistsO (см. главу 8);
□ аллокаторы buddy-системы инициализируются с помощью функций
page_alloc_init () Hmem_init() (см. главу 8)\
П окончательная инициализация таблицы IDT выполняется с помощью
функций trap_init () И init_IRQ () (см. главу 4)\
П TASKLETSOFTIRQ И HISOFTIRQ ИНИЦИаЛИЗИруЮТСЯ С ПОМОЩЬЮ фуНКЦИИ
softirq_init () (см. главу 4)\
□ системные дата и время инициализируются с помощью функции
time_init () (см. главу 6)\
П slab-аллокатор инициализируется С ПОМОЩЬЮ фуНКЦИИ kmemcacheinit ()
(см. главу 8);
П скорость работы процессора определяется с помощью функции
calibrate_delay() (см. главу 6)\
П ПОТОК Ядра ДЛЯ Процесса 1 СОЗДаетСЯ С ПОМОЩЬЮ фуНКЦИИ kernel_thread().
В свою очередь, этот поток создает другие потоки ядра и выполняет про-
программу /sbin/init, как описано в главе 3.
Кроме сообщения "Linux version 2.6.11...", которое выводится на экран сразу
после начала работы функции startjcerneio, на этом этапе выводится и
много других сообщений, как от программы init, так и от потоков ядра. По
окончании работы функции на консоли (или в графическом интерфейсе, если
была запущена система X Window System) появляется всем знакомая под-
подсказка для входа в систему, говорящая пользователю, что ядро Linux загру-
загружено и работает.
ПРИЛОЖЕНИЕ 2
Модули
Как было отмечено в главе 7, модули — это рецепт, предлагаемый операци-
операционной системой Linux для эффективного достижения многих теоретических
преимуществ микроядра без снижения производительности.
Быть (модулем) или не быть?
Когда системным программистам нужно добавить в ядро Linux новую функ-
функциональную возможность, им приходится принимать принципиальное реше-
решение: создавать ли новый код так, что он будет откомпилирован как модуль,
или статически компоновать этот код с ядром.
Вообще, системные программисты стараются реализовать новый код в виде
модуля. Поскольку модули могут быть скомпонованы по требованию (в чем
мы убедимся далее), ядру не приходится "раздуваться" от сотен редко ис-
используемых программ. Почти все высокоуровневые компоненты ядра
Linux— файловые системы, драйверы устройств, форматы исполняемых
файлов, сетевые слои и т. д. — могут быть откомпилированы как модули. Ди-
Дистрибутивы Linux активно используют модули для обеспечения удобной ра-
работы с широким диапазоном аппаратных устройств. Например, в дистрибу-
дистрибутиве в соответствующем каталоге находятся десятки модулей драйверов зву-
звуковых карт, хотя только один из этих модулей будет фактически загружен на
конкретном компьютере.
Тем не менее часть кода Linux обязательно должна быть скомпонована ста-
статически, а это означает, что либо соответствующий компонент включен в яд-
ядро, либо он вообще не откомпилирован. Как правило, это происходит, когда
компонент требует модификации каких-нибудь структур данных или некото-
некоторой функции, статически скомпонованной с ядром.
Предположим, например, что компонент должен добавить новые поля в деск-
дескриптор процесса. Подключение модуля не сможет изменить существующие
структуры, такие как taskstruct, поскольку, даже если модуль будет поль-
пользоваться модифицированной версией структуры, весь статически скомпоно-
скомпонованный код будет по-прежнему видеть старую версию. В результате, скорее
всего, произойдет порча данных. Частичным решением проблемы будет "ста-
"статическое" добавление новых полей в дескриптор процесса, чтобы они были
доступны компоненту ядра, независимо от того, как он был скомпонован.
Однако если этот компонент не используется, то дополнительные поля, раз-
размноженные в каждом дескрипторе процесса, будут непроизводительной тра-
тратой памяти. Если новый компонент ядра значительно увеличивает размер де-
дескриптора процесса, то, с точки зрения производительности системы, разум-
разумнее будет добавить требуемые поля в структуру данных, только если
компонент скомпонован с ядром статически.
В качестве второго примера рассмотрим компонент ядра, который замещает
собой статически скомпонованный код. Совершенно ясно, что такой компо-
компонент не может быть откомпилирован в виде модуля, потому что ядро не в со-
состоянии изменить машинный код, уже находящийся в оперативной памяти,
когда оно подключает модуль. Например, невозможно подключить модуль,
который меняет способ выделения страничных кадров, поскольку функции
buddy-системы всегда статически скомпонованы с ядром1.
При работе с модулями перед ядром стоят две основные задачи. Первая —
сделать так, чтобы остальным компонентам были доступны глобальные сим-
символы модуля, например, точка входа в его главную функцию. Кроме того,
модуль должен знать адреса символов в ядре и других модулях. Таким обра-
образом, при подключении модуля должны быть и навсегда разрешены ссылки.
Вторая задача заключается в слежении за использованием модулей, чтобы ни
один модуль не был выгружен, когда с ним работает другой модуль или дру-
другая часть ядра. Для этой цели служит обычный счетчик ссылок.
Лицензии на модули
Лицензия ядра Linux (GPL, версия 2) либеральна в отношении того, что поль-
пользователи и фирмы делают с исходным кодом. Однако она строго запрещает
распространение исходного кода, происходящего от (или сильно зависящего
1 Вы, возможно, задаетесь вопросом, почему ваш любимый компонент ядра не оформлен в виде
модуля. В большинстве случаев для этого нет серьезных технических причин, и, в сущности, все
сводится к лицензии на программный продукт. Разработчики ядра хотят гарантировать невозмож-
невозможность замены его ключевых компонентов проприетарными модулями, выполненными в виде дос-
доступных только в двоичной форме "черных ящиков".
от) кода Linux, под лицензией, отличной от GPL. В сущности, разработчики
ядра хотят быть уверены в том, что их код и весь код, производный от него,
останется свободно доступным всем пользователям.
Однако модули представляют определенную угрозу этой модели. Кто-нибудь
может выпустить модуль для ядра Linux только в двоичной форме. На-
Например, производитель станет распространять драйвер для своего аппа-
аппаратного устройства в виде двоичного модуля. Уже есть множество таких слу-
случаев. Теоретически характеристики и поведение ядра Linux могут быть зна-
значительно изменены двоичными модулями, а в результате ядро, построенное
на основе ядра Linux, будет фактически превращено в коммерческий про-
продукт.
Одним словом, сообщество разработчиков плохо принимает модули, выпус-
выпускаемые только в виде двоичного кода. Реализация модулей Linux отражает
этот факт. В принципе, каждый разработчик модуля должен в исходном коде
указывать тип лицензии с помощью макроса modulelicense. Если лицензия
несовместима с GPL (или вовсе не указана), модуль не сможет обращаться к
многим важным функциям и структурам данных ядра. Более того, использо-
использование модуля с лицензией, отличной от GPL, "запятнывает ядро, т. е. любая
предполагаемая ошибка в ядре не обсуждается разработчиками ядра.
Реализация модулей
Модули хранятся в файловой системе в виде объектных файлов формата ELF
и компонуются с ядром программой insmod (см. далее разд. "Подключение и
выгрузка модулей"). Для каждого модуля ядро выделяет область памяти, со-
содержащую следующие данные:
□ объект module;
□ строку с завершающим нулевым символом, представляющую имя модуля
(все модули должны иметь уникальные имена);
□ код, реализующий функции модуля.
Объект module описывает модуль; его поля перечислены в табл. П2.1. Все
объекты module организованы в двунаправленный циклический список. Его
голова находится в переменной modules, а указатели на соседние элементы
собраны в поле list каждого объекта module.
2 От английского термина "taint kernel". — Прим. науч. ред.
Таблица П2.1. Объект module
Тип Поле Описание
enum modulestate state Внутреннее состояние
модуля
struct list_head list Указатели для списка
модулей
char [60] name Имя модуля
struct modulekobject mkobj Включает в себя структуру
kobject и указатель на
этот объект-модуль
struct module_param_attrs * param_attrs Указатель на массив
дескрипторов параметров
модуля
const struct kernel_symbol * syms Указатель на массив экс-
экспортированных символов
unsigned int num_syms Количество экспортиро-
экспортированных символов
const unsigned long * crcs Указатель на массив зна-
значений CRC для экспорти-
экспортированных символов
const struct kernel_symbol * gpl_syms Указатель на массив сим-
символов, экспортированных
noGPL
unsigned int num_gpl_syms Количество символов,
экспортированных по GPL
const unsigned long * gpl_crcs Указатель на массив зна-
значений CRC для символов,
экспортированных по GPL
unsigned int numexentries Количество записей в таб-
таблице исключений модуля
const struct exception_ extable Указатель на таблицу ис-
table_entry * ключений модуля
int (*) (void) init Метод инициализации
модуля
void * module_init Указатель на области
динамической памяти, вы-
выделенные для инициализа-
инициализации модуля
void * module_core Указатель на области ди-
динамической памяти, выде-
выделенные для базовых функ-
функций и структур данных мо-
модуля
Таблица П2.1 (продолжение)
Тип Поле Описание
unsigned long init_size Размер области динамиче-
динамической памяти, необходимой
для инициализации модуля
unsigned long coresize Размер области динамиче-
динамической памяти, необходимой
для базовых функций и
структур данных модуля
unsigned long init_text_size Размер выполняемого ко-
кода, необходимого для ини-
инициализации модуля; ис-
используется только при под-
подключении модуля
unsigned long coretextsize Размер выполняемого
кода модуля; используется
только при подключении
модуля
struct mod_arch_specif ic arch Поля, специфичные для
архитектуры (в архитектуре
80x86 — ничего)
int unsafe Флаг, установленный, если
модуль не может быть
безопасно выгружен
int licensegplok Флаг, установленный, если
лицензия модуля совмес-
совместима с GPL
struct module_ref [NR_CPUS] ref Счетчики обращений,
имеющиеся у каждого про-
процессора
struct list_head modules_which_use_me Список модулей,
использующих данный
struct task_struct * waiter Процесс, пытающийся
выгрузить модуль
void (*) (void) exit Метод для выхода из мо-
модуля
Elf_Sym * symtab Указатель на массив ELF-
символов модуля, для фай-
файла /proc/kallsyms
unsigned long numsymtab Количество ELF-символов
модуля, показываемых в
файле /proc/kallsyms
Таблица П2.1 (окончание)
Тип Поле Описание
char * strtab Таблица строк для ELF-
символов модуля, показы-
показываемых в файле
/proc/kallsyms
struct module_sect_attrs * sect_attrs Указатель на массив деск-
дескрипторов атрибутов секций
модуля (показываемых
в файловой системе sysfs)
void * percpu Указатель на области па-
памяти, специфичные для
процессоров
char * args Аргументы командной
строки, используемые
при подключении модуля
Поле state кодирует внутреннее состояние модуля: оно принимает значения
MODULE_STATE_LIVE (МОДУЛЬ аКТИВен), MODULE_STATE_COMING (МОДУЛЬ ИНИЦИаЛИ-
зируется) и module_state_going (модуль удаляется).
Как уже говорилось в главе 10, у каждого модуля есть собственная таблица
исключений. Она содержит адреса обработчиков, входящих в состав модуля,
если таковые имеются. Таблица копируется в оперативную память при под-
подключении модуля, и ее начальный адрес хранится в поле extabie объекта
module.
Счетчики обращений
У каждого модуля есть счетчики обращений, по одному на каждый процес-
процессор. Они хранятся в поле ref объекта module. Счетчик обращений увеличива-
увеличивается, когда начинается операция, в которой задействованы функции модуля,
и уменьшается по окончании операции. Модуль может быть выгружен, толь-
только если сумма его счетчиков равна 0.
Предположим, например, что слой файловой системы MS-DOS откомпили-
откомпилирован в виде модуля, и этот модуль подключается на этапе выполнения. Вна-
Вначале все счетчики обращений сброшены в ноль. Если пользователь монтирует
дискету MS-DOS, один из счетчиков обращений увеличивается на 1. Если же
пользователь размонтирует дискету, один из счетчиков (возможно, не тот,
что был увеличен) уменьшается на 1. Общий счетчик обращений модуля ра-
равен сумме счетчиков по всем процессорам.
Экспортирование символов
При подключении модуля все ссылки на глобальные символы ядра (перемен-
(переменные и функции) в объектном коде модуля должны быть заменены на соответ-
соответствующие адреса. Эта операция во многом аналогичная той, которую выпол-
выполняет компоновщик при компиляции программы режима пользователя (см.
разд. "Библиотеки" главы 20), делегируется внешней программе insmod (опи-
(описанной далее в разд. "Подключение и выгрузка модулей").
Ядро использует специальные таблицы символов ядра для хранения симво-
символов, к которым могут обращаться модули, и адресов этих символов. Они на-
находятся в трех секциях сегмента кода ядра: секция kstrtab содержит имена
символов, секция ksymtab — адреса символов, доступных всем типам моду-
модулей, а секция ksymtabgpi — адреса символов, которыми могут пользовать-
пользоваться модули, выпущенные под GPL-совместимой лицензией. Макросы
export_symbol и export_symbol_gpl, вызванные в статически скомпонованном
коде ядра, заставляют компилятор С заносить указанный символ в секцию
ksymtab И ksymtab_gpl Соответственно.
В таблицу включаются только символы, фактически используемые каким-
либо активным модулем. Если системному программисту понадобится из мо-
модуля обратиться к еще не экспортированному символу ядра, он просто добав-
добавляет соответствующий макрос exportsymbolgpl в исходный код ядра. Ко-
Конечно, он не может легально экспортировать новый символ для модуля,
имеющего лицензию, не совместимую с GPL.
Подключенные модули могут экспортировать свои символы, чтобы другие
модули могли обращаться к ним. Таблицы символов модуля содержатся в сек-
секциях ksymtab, ksymtabgpi И kstrtab Сегмента КОДа МОДУЛЯ. Чтобы ЭКС-
портировать подмножество символов из модуля, программист может вос-
воспользоваться макросами export_symbol и export_symbol_gpl, описанными
ранее. При подключении модуля экспортированные символы модуля копи-
копируются в два массива в оперативной памяти, а их адреса сохраняются в по-
полях syms И gpl_syms объекта module.
Зависимости модулей
Модуль В может ссылаться на символы, экспортированные модулем А. Тогда
мы говорим, что В загружен поверх А или что А используется модулем В.
Чтобы можно было подключить модуль В, модуль А уже должен быть под-
подключен; в противном случае ссылки на символы, экспортируемые моду-
модулем А, не могут быть корректно разрешены в модуле В. Короче говоря, меж-
между модулями существует зависимость.
Поле moduleswhichuseme объекта module МОДУЛЯ А ЯВЛЯетСЯ ГОЛОВОЙ СПИСКа
зависимостей, содержащего все модули, которые используют модуль А. Каж-
Каждый элемент этого списка представляет собой маленький дескриптор
moduieuse, содержащий указатели на соседние элементы списка и указатель
на соответствующий объект module. В нашем примере дескриптор moduieuse,
указывающий на объект module модуля В, будет находиться в списке
modules_which__use_me модуля А. СПИСОК modules_which_use_me должен обнов-
ляться динамически каждый раз, когда какой-либо модуль загружается по-
поверх модуля А. Модуль А не может быть выгружен, если его список зависи-
зависимостей не пуст.
Конечно же, кроме модулей А и В, может существовать еще один модуль
(скажем, С), загруженный поверх В, и т. д. "Накладывание" модулей друг на
друга является эффективным способом модуляризации исходного кода ядра и
ускорения его разработки.
Подключение и выгрузка модулей
Пользователь может подключить модуль к работающему ядру, вызвав внеш-
внешнюю программу insmod. Эта программа выполняет следующие действия:
1. Читает из командной строки имя модуля, который нужно подключить.
2. Находит в системном дереве каталогов файл с объектным кодом модуля.
Как правило, файл находится в каком-нибудь подкаталоге каталога
/lib/modules.
3. Читает с диска файл, содержащий объектный код модуля.
4. Делает системный вызов initmoduie (), передавая ему адрес буфера ре-
режима пользователя, содержащего объектный код модуля, размер объект-
объектного кода и область памяти режима пользователя, содержащую парамет-
параметры программы insmod.
5. Завершает работу.
Всю реальную работу делает служебная процедура sysinitmoduie (). Она
выполняет следующие основные действия:
1. Проверяет, разрешено ли пользователю подключать модуль (текущий
процесс должен иметь способность capsysmodule). В любой ситуации,
в которой кто-то добавляет к ядру новую функциональную возможность,
вопросы безопасности выходят на первый план.
2. Выделяет область памяти под объектный код модуля. Затем копирует в
нее данные из буфера режима пользователя, переданного системному вы-
вызову в качестве первого параметра.
3. Убеждается, что данные в области памяти фактически представляют со-
собой ELF-объект модуля. Если это не так, возвращает код ошибки.
4. Выделяет область памяти под параметры, переданные программе insmod,
и заполняет ее данными из буфера режима пользователя, переданного
системному вызову в качестве третьего параметра.
5. Перебирает элементы списка modules, чтобы убедиться, что модуль еще
не подключен. Проверка выполняется путем сравнения имен модулей
(полей name у объектов module).
6. Выделяет область памяти под основной выполняемый код модуля и за-
заполняет ее содержимым соответствующих секций модуля.
7. Выделяет область памяти под код инициализации модуля и заполняет ее
содержимым соответствующих секций модуля.
8. Определяет адрес объекта module для нового модуля. Образ этого объекта
включен в секцию gnu.iinkonce.thismoduie сегмента текста в ELF-файле
модуля. Таким образом, объект module находится в области памяти, за-
заполненной на шаге 6.
9. Записывает в поля moduiecode и moduieinit объекта module адреса об-
областей памяти, выделенных на шагах 6 и 7.
10. Инициализирует СПИСОК modules_which_use_me объекта module И заПИСЫВа-
ет нули во все счетчики ссылок модуля, кроме счетчика, выполняющего
процессора. Этот счетчик устанавливается в единицу.
11. Устанавливает флаг licensegpiok объекта module в соответствии с типом
лицензии, указанным в объекте module.
12. Пользуясь таблицами символов ядра и таблицами символов модуля, пе-
перемещает объектный код модуля. Это означает замену всех вхождений
внешних и глобальных символов на соответствующие смещения логиче-
логических адресов.
13. Инициализирует поля syms и gpisyms объекта module так, чтобы они ука-
указывали на находящиеся в памяти таблицы символов, экспортированных
модулем.
14. Таблица исключений модуля (см. разд. "The Exception Tables" главы 10)
находится в секции extabie ELF-файла модуля; она была скопирована
в область памяти, выделенную на шаге 6. Описываемая функция сохраня-
сохраняет адрес таблицы В ПОЛе extabie объекта module.
15. Анализирует аргументы программы insmod и устанавливает значения со-
соответствующих переменных модуля.
16. Регистрирует объект kobject, хранящийся в поле mkobj объекта module,
чтобы новый подкаталог для модуля появился в каталоге module специ-
специальный файловой системы sysfs (см. разд. "Объекты kobject" главы 13).
17. Освобождает временную область памяти, выделенную на шаге 2.
18. ЗанОСИТ объект module В СПИСОК modules.
19. Устанавливает состояние модуля в значение module statecoming.
20. Выполняет метод init объекта module, если метод определен.
21. Устанавливает состояние модуля в значение modulestatelive.
22. Заканчивает работу, возвращая 0 (успешное завершение).
Чтобы выгрузить модуль, пользователь вызывает внешнюю программу
rmmod, которая выполняет следующие действия:
1. Читает из командной строки имя модуля, который нужно выгрузить.
2. Открывает файл, в котором перечислены все модули, подключенные к яд-
ядру, и убеждается, что модуль, подлежащий выгрузке, действительно под-
подключен.
3. Делает системный вызов deietemoduie (), передавая ему имя модуля.
4. Завершает работу.
Служебная процедура sysdeietemoduie () выполняет следующие действия:
1. Проверяет, разрешено ли пользователю выгружать модуль (текущий про-
процесс должен иметь способность capsysmodule).
2. Копирует имя модуля в буфер ядра.
3. Просматривает список modules в поисках объекта module данного модуля.
4. Проверяет СПИСОК modulesjtfhich_use_me МОДУЛЯ. ЕСЛИ СПИСОК не пуст, ВОЗ-
вращает код ошибки.
5. Проверяет состояние модуля. Если оно не равно modulestatelive, воз-
возвращает код ошибки.
6. Если у модуля есть метод init, функция проверяет, есть ли у него и метод
exit. Если метод exit не определен, модуль не следует выгружать, и
функция возвращает код выхода.
7. Чтобы избежать конфликтов, прекращает деятельность всех процессоров в
системе, кроме процессора, выполняющего служебную процедуру
sys_delete_module().
8. Устанавливает состояние модуля в значение modulestategoing.
9. Если сумма всех счетчиков ссылок модуля больше нуля, возвращает код
ошибки.
10. Выполняет метод exit, если он определен у модуля.
11. Удаляет объект module из списка modules и отменяет регистрацию модуля
в специальной файловой системе sysfs.
12. Удаляет объект module из списка зависимостей модулей, которыми он
пользовался.
13. Освобождает области памяти, которые содержат выполняемый код моду-
модуля, объект module и разнообразные таблицы символов и исключений.
14. Возвращает ноль (успех).
Подключение модулей по требованию
Модуль может быть подключен автоматически, когда потребуется обеспечи-
обеспечиваемая им функциональность, а затем автоматически удален.
Предположим, например, что файловая система MS-DOS не была подключе-
подключена ни статически, ни динамически. Если пользователь попытается смонтиро-
смонтировать файловую систему MS-DOS, системный вызов mount () нормальным об-
образом закончится неудачей и возвратит код ошибки, потому что MS-DOS не
находится в списке f iiesystems зарегистрированных файловых систем. Од-
Однако если при конфигурировании ядра была задана поддержка автоматиче-
автоматического подключения модулей, Linux попытается подключить модуль MS-DOS,
а затем снова просмотрит список зарегистрированных файловых систем. Если
модуль был подключен успешно, системный вызов mount () будет выполнен
так, словно файловая система присутствовала с самого начала.
Программа modprobe
Чтобы автоматически подключить модуль, ядро создает поток для выполне-
выполнения внешней программы modprobe3, которая разберется со всеми вопросами,
связанными с зависимостями модулей. Зависимости обсуждались ранее: мо-
модулю для работы может потребоваться один или несколько модулей, а тем, в
свою очередь, могут потребоваться еще модули. Например, модулю MS-DOS
требуется другой модуль по имени fat, содержащий код, общий для всех фай-
файловых систем, основанных на FAT (File Allocation Table, Таблица размеще-
размещения файлов). То есть если модуля fat еще нет, то он тоже должен быть авто-
автоматически подключен при запросе на модуль MS-DOS. Разрешение зависи-
зависимостей и поиск модулей лучше всего выполнять в режиме пользователя,
поскольку эти действия связаны с обращением к объектным файлам модулей
в файловой системе.
3 Это один из немногих примеров обращения ядра за помощью к внешней программе.
Внешняя программа modprobe аналогична программе insmod, поскольку
подключает модуль, указанный в командной строке. Однако программа
modprobe вдобавок рекурсивно подключает все модули, используемые моду-
модулем, заданным в командной строке. Например, если пользователь вызовет
modprobe для подключения модуля MS-DOS, эта программа подключит вслед
за MS-DOS и модуль fat, если это будет необходимо. Программа modprobe
просто проверяет зависимости модуля; фактически его подключение проис-
происходит за счет ответвления нового процесса и выполнения программы insmod.
Откуда программа modprobe знает о зависимостях модуля? При запуске сис-
системы выполняется еще одна внешняя программа по имени depmod. Она про-
просматривает все модули, откомпилированные для работающего ядра, которые
обычно хранятся в каталоге /lib/modules. Затем эта программа записывает все
зависимости модулей в файл modules.dep. Программа modprobe просто со-
сопоставляет информацию из этого файла со списком подключенных модулей,
полученным из файла /proc /modules.
Функция request_module()
В некоторых случаях ядро вызывает функцию requestmoduie (), пытаясь ав-
автоматически подключить модуль.
Снова рассмотрим пример, в котором пользователь пытается смонтировать
файловую систему MS-DOS. Если функция getf stype () обнаруживает, что
файловая система не зарегистрирована, она вызывает функцию
requestmoduie () в расчете на то, что компонент MS-DOS был откомпилиро-
откомпилирован в виде модуля.
Если функции requestmoduie () удается подключить запрошенный модуль,
функция gerf stype () продолжит работу, словно модуль всегда присутство-
присутствовал в системе. Конечно, такое случается не всегда. В нашем примере не ис-
исключено, что модуль MS-DOS вообще не был откомпилирован. Тогда функ-
функция getf stype () ВОЗВраТИТ КОД Ошибки.
В качестве параметра функция requestmoduleo принимает имя модуля, ко-
который НуЖНО ПОДКЛЮЧИТЬ. Она ВЫПОЛНЯет фуНКЦИЮ kernel_thread() ДЛЯ СОЗ-
дания нового потока ядра и ждет, пока он завершится.
Поток ядра, в свою очередь, принимает в качестве параметра имя подклю-
подключаемого модуля и делает системный вызов execve (), чтобы выполнить внеш-
внешнюю программу modprobe, передавая ему имя модуля. Со своей стороны,
программа modprobe фактически подключает запрошенный модуль и все мо-
модули, от которых он зависит.
Предметный указатель
— _GFP_DMA 405
_add_to_swap_cache() 926 _GFP_FS 426
_alloc_pages() 423, 424, 875, 886, 900 _GFP_HIGHMEM 405
_bforget() 781 _GFP_NOFAIL 426
_block_commit_write() 830 _GFP_NORETRY 425
_blockdev_direct_IO() 850 _GFP_REPEAT 426
__bread() 788, 952 _GFP_WAIT 424
__brelse() 781 _group_complete_signal() 566
_copy_from_user() 827 _KERNEL_CS 79, 524
__copy_to_user() 811 _KERNEL_DS 79
_d_lookup() 615 _kernel_vsyscall() 529
do_IRQ() 228 makejrequest() 744
_do_softirq() 242 _NR_write 544
_down() 286, 287, 288 _pa 113
_exit_files() 182 __pdflush() 793
_exit_fs() 182 _Pgd Ю0
_exit_sighand() 184 __PgP™t 100
_exit_signal() 184 __pmd 100
_find_get_block() 786, 879 _pte 100
_flush_tlb() 120 _Pud 100
_flush_tlb__all() 113 __put_user_asm() 537
_flush_tlb_global() 120 __put_user_u64() 537
_flush_tlb_single() 120 __read_lock_failed() 279
_free_page() 405 __register_chrdev_region() 711
_free_pages() 405, 426 __rmqueue() 415
_free_pages_bulk() 417 _switch_to() 157
_generic_file_aio_read() 807, 848, 849 __unhash_process() 184
_generic_file_aio_write_nolock() 825 _unlazy_fpu() 158, 172
getcpuvar(name) 267 UP() 285
_get_dma_pages() 403 _USER_CS 79
_get_free_page() 403 _USER_DS 79
_get_free_pages() 403, 406 _va 113
_get_user_l() 536, 541 _vfs_follow_link() 648
__getblk() 787, 874 _vma_unlink() 483
__GFP_COLD 420 _vunmap() 457
_wait_on_bit_lock() 773 alloc_page() 403, 406, 454
writeback_single_inode() 797 alloc_page_buffers() 784, 874
_exit() 62, 178, 181, 463 alloc_pages() 403, 406, 776
_llseek() 594 alloc_percpu(type) 267
syscalB 544 alloc_slabmgmt() 436
alloc_task_struct() 172
д alloc_vfsmnt() 626, 630
alpha 35
accessQ 593 anon_pipe_buf_ops 987
access_ok() 535, 808 anonvma 865
Accessed 86, 412 Anticipatory 741
account_it_prof() 325, 342 API 519
account_it_virt() 325, 342 APIC 192, 222, 311
account_system_time() 325 apic_intr_init() 321
account_user_time() 325 apic_timer_interrupt() 321, 322
ack 228 АРМ 82
ACPI 313,1054 arch_get_unmapped_area() 481
activate_page() 876 arch_get_unmapped_area_topdown() 481
activate_task() 366 arch_pick_mmap_layout() 1049
add_disk() 728, 755 arm26 35
add_page_to_active_list() 876 arraycache 442
add_page_to_inactive_list() 876 asm 271
add_timer() 329, 342 atomic_add(i,v) 269
addjo jpage_cache() 773, 776, 809 atomic_add_negative(i, v) 269
add_to_swap() 927 atomic_add_return(i, v) 269
add_to_swap_cache() 926, 932 atomic_clear_mask(mask, addr) 270
add_wait_queue() 145 atomicdec(v) 269
add_wait_queue_exclusive() 145 atomic_dec_and_test(v) 269
address_space 765, 864 atomic_dec_return(v) 269
ADFS 589 atomicinc(v) 269
adjtimex() 320, 340 atomicincandtest(v) 269
AFFS 589 atomic_inc_return(v) 269
AFS 589 atomicread(v) 269
AGP 698 atomic_set(v,i) 269
aio cancelQ 851 atomic_set_mask(mask, addr) 270
aio~error() 851 atomic_sub(i,v) 269
aio_fsync() 851 atomic_sub_and_test(i, v) 269
aio_pread() 855 atomic_sub_return(i, v) 269
aio_read() 851 attach_pid() 142, 175
aio_return() 851
aio_suspend() 851 g
aio_write() 851
alarm() 342 background_writeout() 794, 795
alloc 449 backingdevinfo 819
alloc_buffer_head() 780 bad_pipe_r() 998
alloc_chrdev_region() 710 bad_pipe_w() 998
alloc_disk() 728 balance_pgdat() 897
allocinode 954, 956 barrier() 270
bd_acquire(inode) 760 Q
bd_claim(O50,912
bd_release() 750,915 cache_alloc_refill() 444
bdev 619, 750 cachechainsem 433
bdget() 751 cache_flusharray() 445
biendio 834 cache_grow() 436, 441
binfhtmisc 619 cacheinitobj s() 436
bio 723, 928 cache_reap() 898
bio_alloc() 725, 728, 813 CAE 30
bio_endio() 758 calc_load() 326
bioforeachsegment 725, 757 calc_vm_prot_bits() 486
biojput() 725, 814 calibrate_APIC_clock() 321
biovec 724 calibrate_tsc() 309
BIOS 107, 1054 call 80
blk_congestion_wait() 426, 739, 886 call_function_interrupt() 235
blk_get_request() 739 CALL_FUNCTION_VECTOR 235
blk_init_queue() 754 call_rcu() 283, 437, 895
blk_partition_remap() 729 calloc() 515
blk_plug_device() 740 can_migrate_task() 384
blk_put_request() 739 cancel_delayed_work() 250
blk_queue_bounce() 744, 746, 747 CAP_AUDIT_CONTROL 1027
blk_remove_plug() 740 CAP_AUDIT_WRITE 1027
blk_unplug_timeout() 740 CAP_CHOWN 1027
blk_unplug_work() 740 CAP_DAC_OVERRIDE 1027
blkdev_get_block() 815 СAP_DAC_READ_SEARCH 1027
blkdev_inode_operations 958 CAP_FOWNER 1027
blkdev_open() 760 CAP_FSETID 1027
blkdev_readpage() 814 СAP_IPC_LOCK 1027
blkdev_writepage() 833 CAP_IPC_OWNER 1027
block_device 748, 912 СAP_KILL 1027
block_device_operations 726 CAPLEASE 1027
block_prepare_write() 829, 977 CAPLINUXJMMUTABLE 1027
block_read_fulljpage() 814, 815, 816 CAPMKNOD 1027
blockwaitqueuerunningO 729 CAPNETADMIN 1027
block_write_fulljpage() 833 CAP_NET_BIND_SERVICE 1027
blockable_page_cache_readahead() 823 CAPNETBROADCAST 1027
brk() 66, 463, 516 CAP_NET_RAW 1027
BSD 589, 655, 937 CAP_SETGID 1027
bsfl 374 CAP_SETPCAP 1027
buddy-система 414 С AP_SETUID 1027
buffered_rmqueue() 420, 423 СAP_SYS_ADMIN 911, 1027
BUILDJNTERRUPT 236 CAP_S YS_BOOT 1027
bus_for_each_dev() 686 CAP_SYS_CHROOT 1027
bus_for_each_drv() 686 CAP_SYS_MODULE 1027
bus_subsys 685 CAP_SYS_NICE 386, 1028
busjype 685 CAP_SYS_PACCT 1028
busiest 383 CAP_SYS_PTRACE 1028
BYTES_PER_WORD 439 CAP_SYS_RAWIO 1028
CAP_SYS_RESOURCE 1028 CLONE_SYSVSEM 168
CAP_SYS_TIME 1028 CLONEJTHREAD 168
CAP_SYS_TTY_CONFIG 1028 CLONEJJNTRACED 168
capvmenoughmemoryO 914 CLONEVFORK 167
capable() 386, 1029 CLONE_VM 167, 512
capget() 1028 close 471
capset() 1028 close() 50, 593, 654, 983, 990, 996, 1020
cascade() 333 Coda 589
CD 95 Coherent 589
cdev 707 commitwrite 827, 828, 831, 978
cdev_add() 708 complete() 291
cdev_alloc() 708 completion 290
CFLGS_OFF_SLAB 432, 436 cond_resched() 244, 425, 809, 828
CFQ 741 context_switch() 376, 377
chacl() 947 copy_files() 173
change_bit(nr, addr) 270 copy_from_user() 335, 584
chardevicestruct 710 copy_fs() 173
chdir() 593 copy_mm() 173, 512, 513
chmod() 593 copy_namespace() 173
chown() 593 copy_page() 509
chrdevinodeoperations 958 copy_page_range() 514
chrdev_open() 712 copy_process() 169, 171, 360, 363
chroot() 593 copy_semundo() 173
CIFS 589, 610 copy_sighand() 173
class 686 copy_signal() 173
classdevice 686 copy_thread() 173, 177
classsubsys 686 copy_to_user() 389
clear_bit(nr, addr) 270 coredump 1042
clear_inode() 896, 963 cp 649
clear_page_range() 107 CPL 200
clear_tsk_need_resched() 375 cpu_idle() 179
ClearPageXyz 394 cpu_relax() 275
cli 233, 292, 327 cpu_rq(n) 356
clock_getres() 343 cpuworkqueuestruct 248
CLOCKMONOTONIC 343 creat() 49, 593
CLOCK_REALTIME 343 create_empty_buffers() 815, 874
clone() 166, 512, 554 create_singlethread_workqueue() 249
CLONE_CHILD_CLEARTID 168 create_workqueue() 249
CLONE_CHILD_SETTID 168 cris 36
CLONE_DETACHED 168 curjimer 315,337
CLONE_FILES 167 current 132, 227, 992, 994
CLONEFS 167 CURRENTBONUS 368
CLONE_NEWNS 168 current_thread_info() 131, 227, 261
CLONE_PARENT 168
CLONE_PARENT_SETTID 168 r\
CLONEPTRACE 167
CLONE_SETTLS 168 d_lookup() 615
CLONE_SIGHAND 167 dcachejock 614
CLONESTOPPED 168 de_thread() 1048
deactivate_task() 372 dma_sync_single_for_cpu() 705
Deadline 741 dma_sync_single_for_device() 705
dec 268 dma_unmap_page() 705
decb 276 dma_unmap_single() 704
DECLAREMUTEX 284 do_add_mount() 629
DECLARE_WAIT_QUEUE_HEAD() 145 do_anonymous_page() 505, 878
decompress_kernel() 1059 do_brk() 517, 1050
def_blk_fops 759 do_each_task_pid() 142
deffifofops 997 do_execve() 1046
default_wake_function() 145 do_exit() 181, 184, 500, 1008
DEFINE_PER_CPU(type, name) 267 do_file_page() 847
del_page_from_active_list() 876 do_follow_link() 649
del_singleshot_timer_sync() 330, 331 do forw\ \^g^ \jj
del_timer_sync() 182, 330 dolgeneral_protection() 160
delay 315,337 do ric file read() ш 824 879
delete_from_swap_cache() 918, 926, 928 doIgettimeofdayO 338
delete_inode 956 do group exit() 181
delete_module() 1070 do^LbdaiiceO 223
dentry610 d0 kem mount(N29
dentry_cache 610 do~lookup() 642
den ry_hashtable 614 do"mmap() 479, 484, 485, 853, 1015, 1049
dentry_open() 652 692, 760, 848, 1047 do-mmap^goff() 485, 838
den ^operations 612 do"mount() 628
dentry unused 614 , - v/ ^4 ,oo
, j . , л.„л do move mount() 628
dependent sleepen) 372 _, - - /4 .j-' лпп „лг
dequeue signal?) 568 do_munmap() 479 488, 916
dequeue"task() 136, 363, 385 J°-neW-m^nt() Ш
destroy Tnode 956 do_nmi() 327
destroy-inode() 896 do_nOJ5age() 505, 840, 1017
destroy"workqueue() 249 do jage_cache_readahead() 843
detachjidQ 142, 184 doj)age_fault() 493, 499, 540
detach vmas to be unmappedO 490 do_pipe() 989
deviceldriver68 " do_sched_setscheduler() 388, 389
device_register() 683 do_sigaction() 582
device_unregister() 683 do_signal() 568
devices_subsys 682 do_signal_stop() 570
devpts 619 do_soffirq() 241, 244, 293
die() 211 do_swap_page() 879, 902, 929
directIO 850 do_syscall_trace() 524
Directory 84, 87, 88 do_timer_interrupt() 320, 322, 326
Dirty 86, 412 do_trap(J10
disable_irq_nosync() 219 do_umount() 636
dlopen() 1032 do_wp_page() 508
DMA 72, 413, 700 down() 58, 285
dma_alloc_coherent() 704 down_interruptible() 288
dma_free_coherent() 704 down_read() 290
dma_map_page() 705 down_read_trylock() 290, 497
dma_map_single() 704 down_trylock() 288
dma_set_mask() 703 down_write() 290
down_write_trylock() 290 EXPORT_SYMBOL_GPL 1067
downgrade_write() 290 Ext2 588, 936
DPL 200, 204, 524 ext2_alloc_block() 968
dummy_security_ops 1029 ext2_alloc_inode() 954
dup() 593, 607, 616 ext2_dir_entry_2 948
dupmmapO 514 ext2_dir_inode_operations 957
dup_task_struct() 172 ext2_flle_inode_operations 957
dup2() 593, 616, 984 ext2_file_operations 958
ext2_fill_super(N31,952
С ext2_free_blocks() 969
ext2_free_inode() 963
e2fsck 940, 971, 976 ext2_get_block() 829, 968
effective_prio() 364 ext2_group_desc 943
ELF 528, 1041, 1063 ext2_inode_info 953
elv_merge() 744 ext2_new_inode() 960
elv_queue_empty() 744 ext2_preread_inode() 962
enable_irq() 219, 231 ext2_sb_info 952
enable_sep_cpu() 527 ext2_super_block 940
end 230 ext2_truncate() 969
end_8259A_irq() 220, 230 ext2_xattr_entry 946
end_buffer_async_read() 817 Ext3 588, 970
end_swap_bio_write() 928 ext3_get_block() 977
end_that_request_chunk() 758 ext3_ordered_commit_write() 979
end_that_request_first() 758 ext3_prepare_write() 977
end_that_request_last() 759
enqueue_task() 136, 385 p
error_code 210
ESCAPE 161 F_GETLK64 663
eventpollfs 619 F_SETLK64 663
events 179, 251 F_SETLKW64 663
exception_table_entry 540 FASYNC 650
exec 1045 FAT 1071
exec_mmap() 1049 fchdir() 593
exec_permission_lite() 641 fchmod() 593
execve() 179, 463, 528, 984, 1046, 1072 fchown() 593
exit() 180 fcntl() 593, 610, 616, 655, 804
EXIT_DEAD 355 fd() 48
exit_group() 180, 1032 fd_set 617
exit_itimers() 184 fdatasync() 594, 801
exit_mm() 182,514 fdformat 954
exit_namespace() 182 fget() 617
exit_notify() 182 fget_light() 618,653
exit_sem() 182, 1008 fgetxattr() 594, 947
exit_thread() 182 fifo_inode_operations 958
EXIT_ZOMBIE 355 fifo_open() 997
expand_stack() 498 FIFO-файл 995
EXPIRED_STARVING 364 0 операции 997
EXPORTSYMBOL 1067 0 создание 996
file 605 flush_signal_handlers() 1049
filejock 657 flush_sigqueue() 560
file_operations 608 flush_thread() 1049
file_ra_state 818 flushtlb 119
file_ra_state_init() 652 flush_tlb_all 119
file_read_actor() 811 flush_tlb_kernel_range 119
file_system_lock 621 flush_tlb_mm 119
filesystemtype 620 flush_tlb_page() 844
file_systems 1071 flush_tlb_pgtables 119
filemap_fdatawait() 845 flush_tlb_range 119
filemap_fdatawrite() 845 flush_workqueue() 250
filemap_nopage() 841, 878, 902 fn() 178
filemap_populate() 846 follow_mount() 642, 643
filemap_sync() 844 follow_page() 488
filesjock 607 foo751
files_stat 607 foo_interrupt() 696
files_struct 615, 1048 foo_read() 696
filler 776 foo_start_dma_transfer() 757
flip 429 foo_strategy() 755
filpcachep 607 for_each_process 135
filp_close() 654 force_sig() 560
filp_open() 651, 914 force_sig_info() 499, 560
flnd_busiest_group() 383 force_sig_speciflc() 560
fmd_busiest_queue() 383 fork() 62, 169, 463, 507, 512, 532, 983
find_get_page() 772, 787, 809, 843 formats 1047
find_get_pages_tag() 778 FPU 161
find_group_other() 961 fput() 617, 990
find_lock_page() 773, 827 fput_light() 618
find_or_create_page() 773, 783, 784 free 449
find_process_by_pid() 388, 390 free() 516
find_task_by_pid() 142, 387 free_block() 445
find_task_by_pid_type() 142 free_buffer_head() 780, 785
fmd_trylock_page() 773 free_cold_page() 421, 892
find_vma() 479, 480, 482 free_hot_cold_page() 422
find_vma_intersection() 480 free_hot_page() 421, 427
fmd_vma_prepare() 480, 483, 486 free_irq() 233
find_vma_prev() 480 free_more_memory() 884, 886
fmish_task_switch() 176, 377, 515 free_page() 405
finish_wait() 146, 897, 992, 994 freejpage_and_swap_cache() 926
fix_to_virt() 117 freejpages() 405
FL_FLOCK 659 freejpages_and_swap_cache() 493, 926
flistxattrO 947 free_pages_bulk() 422, 427
fllistxattr() 594 free_percpu(pointer) 267
flock() 594, 655, 659 free_pgtables() 493
floppy_interrupt() 234 free_swap_and_cache() 493, 926
flush_all_zero_pkmaps() 410 free_vfsmnt() 626
flush_old_exec() 1048 fremovexattr() 594, 947
flush_old_files() 1049 frstor 165
frv 36 getdents() 593
fsstruct 615 getfacl() 947
fsetxattr() 594, 946 getname() 650
fstat() 593 getpid() 129
fstatfs() 593 getpriority() 347, 386
fsync() 594, 801 getrlimit() 150
ftime() 338 gettimeofday() 338
ftruncate() 593 getxattr() 594, 947
futexfs 619 grab_swap_token() 843, 902
fxrstor 165 graft_tree() 629
group_send_sig_info() 564, 581
Q grow_buffers() 783, 784, 874
grow_dev_page() 783
GART698 GRUB 1056
GDT80, 153
gendisk 725, 753 |-|
generic_commit_write() 830
genericfileaiowritenolock 849 h8300 36
generic_file_direct_IO() 849 handle_io_bitmap() 160
generic_file_read() 805, 848 handlemm _fault() 488, 501, 930
generic_file_write() 824, 977 handle_pte_fault() 508, 847, 929, 930
generic_make_request() 728, 744 handle_ra_miss() 809, 824
generic_unplug_device() 740 handle_signal() 571
getblock 812 handle_stop_signal() 565
get_cmos_time() 318 hardirq_ctx 224
get_cpu() 262 hardirq_stack 224
getcpuvar(name) 267 hash_long() 139
get_device() 683 hdstruct 727
get_empty_filp() 607 HFS 5 89
get_fs_type() 630, 1072 HISOFTIRQ 238, 245, 246
getjiffies_64() 317 hlistjiead 134
getoffset 315,338 hlistnode 134
get_pipe_inode() 989 hit 179
get_sb_bdev() 631 HOME 1022
get_sb_nodev() 631 hotplug 686
get_sb_pseudo() 631 HPET 312,319
get_sb_single() 631 hpet_enable() 319
get_sigframe() 574 HPFS 589
get_swap_bio() 928, 933 hwinterrupttype 220
get_swap_page() 921 hw_irq_controller 220
get_thread_area() 82 hw_resend_irq() 231
get_unmapped_area() 481, 839
get_unused_fd() 651 I
get_user() 536
get_user_pages() 488 ICLEAR 603
get_vm_area() 452, 453 IDIRTY 603
get_zeroed_page() 403, 428 IDIRTYDATASYNC 602
getcwd() 593 IDIRTYPAGES 602
I_DIRTY_SYNC 602 inode_unused 603
IFREEZING 603 ins 669
ILOCK 603 insb() 670
INEW 603 insert_vm_struct() 483
i386 36 insl() 670
ia64 36 insmod 1067, 1068
ICR194 insw(N70
idle_balance() 372 interrupt 225
IDT 198, 204 interruptible_sleep_on() 146
idttable 206 interruptible_sleep_on_timeout() 146
ignore_int() 206 invalidate_inode_pages2() 850
in 669 invalidate_interrupt() 235
in_atomic() 496 INVALIDATETLBVECTOR 235
in_interrupt() 240, 241 invlpg 120
inb() 670 inw() 670
inb_p() 670 inw_p() 670
inc 268 io_cancel() 594, 852
info 904 io_destroy() 594, 852
init 63, 179 io_getevents() 594, 852
INITJFILES 178 io_setup() 594, 852
init_fpu() 165 io_submit() 594, 608, 852, 854
INIT_FS 178 iocb 854
init_IRQ() 219, 319 ioctl() 590
init_mm 916 IO-MMU 702
init_module() 1068 ioremap() 699
init_mount_tree() 632 ioremap_nocache() 699
init_MUTEX() 284 ipc() 1003
init_MUTEX_LOCKED() 284 ipc_id_ary 1002, 1006, 1010, 1014
init_new_context() 514, 1047 ipcids 1001, 1005, 1010, 1014
init_pipe_fs() 988 IPC_INFO 1003
init_rootfs() 632 IPC_RMID 1003
init_rwsem() 290 IPC_SET 1003
INITSIGHAND 178 IPC_STAT 1003
INIT_SIGNALS 178 IPCMNI 1001
init_special_inode() 692, 997 iput() 613
initsynckiocb 805 iret 523
INITTASK 178 IRQ 191, 212, 218, 222, 233
INITTHREADINFO 178 irq_cpustat_t 221, 240
init_timer() 329 irq_desc_t 217
init_waitqueue_entry() 145 IRQDISABLED 229
init_waitqueue_func_entry() 145 irq_enter() 226, 323
init_waitqueue_head() 145 irq_exit() 227, 241, 323
inl() 670 IRQ_INPROGRESS 229
inl_p() 670 IRQ_PENDING 229
inode_hashtable 603, 962 IRQ_REPLAY 229
inodejn_use 603 IRQ_WAITING 229
inodeoperations 601 irqaction 220, 233
irqs_disabled() 292 kmem_cache_destroy() 433
ISO 09660 589 kmem_cache_free() 445, 448, 449,491
itreal fn() 342 kmem_cache_init() 433
ITIMERPROF 341 kmem_cache_reap() 304
ITIMERREAL 341 kmem_cache_shrink() 304, 434
ITIMERVIRTUAL 341 kmem_cache_t 429, 898
kmem_freepages() 435, 437
■ kmem_getpages() 434, 436
kmem_list3 431
JFS 589 kobjJookupO 709
jiffies 316, 695 kobj_map() 709
journal_commit_transaction() 979 kobj_type 678
journal_get_write_access() 978 kobject 677
journaljiead 974 kobject_get() 678
journal_start() 975 kobject_put() 678
journal_stop() 975 kobject_register() 680
kobject_unregister() 681
u kset 678
К kset_get() 679
к ref 678 tSVUti\Z9
- . .. ... ksoftirqd 180
k_sigacion555 ksoftirqd() 243
tTlH kswaPdl80,424,897
?blockd180 kunmap(L11,811,987
kern ipc_j)erm 1002, 1006, 1011 kunmap atomic() 412, 757
kernel_fpu_begin() 165 к_набор 6?8
kerneLfpu_end() 165 к-объеьсг 677
kernelJhreadO 177, 1072
keventd 179 .
kfree() 447, 457 L
^•nTrf7 854Cn L1_CACHE_BYTES 118
kill() 548, 580 LARGEPAGEMASK 99
kill_pg() 561 LARGE_PAGE_SIZE 99
kill_pg_info() 561, 581 LAST_BIND 646
kill j)roc() 561 LAST_DOT 646
kill_proc_info() 561, 581 LAST_DOTDOT 646
kill_something_info() 580 LAST_NORM 646
kiocb 805, 854 LASTJPKMAP 407
kioctx 854 LASTROOT 646
kirqd 223 lchown() 593
kmalloc() 447, 452, 454 Ichownl6() 593
kmap() 408, 811, 987 LDT 74, 82
kmap_atomic() 412,757 lgetxattr() 594, 947
kmap_high() 408 Hbc 520
kmaplock 409 Hbm 1032
kmem_cache_alloc() 443, 447, 449, 468, 487, HbXl 1 1032
492 LILO 1056
kmem_cache_create() 304, 433,439, 442 Hnk() 593
link_path_walk() 640 ОД
linuxbinfmt 1041
linuxbinprm 1047 m32r 36
list_add() 133 m68k 36
list_add_tail() 133, 363, 436 m68knommu 36
list_del() 133, 363 machine_speciflc_memory_setup() 108
list_empty() 133 MADV_NORMAL 843
list_entry 148 MADV_RANDOM 843
list_for_each 148 MADV_SEQUENTIAL 843
list_for_each_entry() 134 madvise() 594, 843
listjiead 132 magic 904
listxattr() 594, 947 main() 1030
ll_rw_block() 790 MAJOR 690
llistxattrO 594, 947 make 340
load_balance() 361,383 make_pages_present() 488
load_binary 1041, 1042, 1047 makejrequestfn 729, 744
load_elf_interp() 1050 malloc() 66, 515
load_script() 1042 mallocsizes 433, 447
loadshlib 1041 map 994
local_bh_disable 293 MAPANONYMOUS 484
local_bh_enable() 241, 293 map_area_pte() 456
local_irq_disable() 243, 292 map_area_pud() 455
local_irq_enable() 243, 292 MAP_DENYWRITE 484
local_irq_restore 241, 242, 246, 292 MAP_EXECUTABLE 484
local_irq_save 240, 241, 246, 292 MAP_FIXED 484
local_softirq_pending() 240, 241, 244 MAPGROWSDOWN 484
lock_kernel() 302 MAPLOCKED 484
lock_page() 773, 810, 843 map_new_virtual() 409
lockf() 655 MAP_NONBLOCK 485
LOOKUP_ACCESS 639 MAPNORESERVE 484
LOOKUP_CONTINUE 639 MAP_POPULATE 484
LOOKUPCREATE 639 MAPPRIVATE 484
lookup_dcookie() 593 MAP_SHARED 484
LOOKUP_DIRECTORY 639 map_vm_area() 454
LOOKUP_FOLLOW 639 mark_inode_dirty() 962
lookup_mnt() 626 markoffset 315,319, 324
LOOKUP_NOALT 639 mark_page_accessed() 776, 787, 811, 828,
LOOKUP_OPEN 639 843, 878, 931
lookup_swap_cache() 926, 930 mask_and_ack_8259A() 220, 228
lremovexattr() 594, 947 match 686
lru_add_drain() 492, 882, 888 math_state_restore() 164
lru_cache_add() 776, 809, 877 MAX_SWAPFILES 904
lru_cache_add_active() 506, 877, 932 mb() 272
Is 983 MBR 1055
lseek() 49, 594 media_changed 753
lsetxattr() 594, 946 memcpy_fromio() 700
lstat() 593 memcpy_toio() 700
LVM 723 mempool_alloc_slab() 449
mempool_create() 449 mq_receive() 1019
mempool_destroy() 450 mq_send() 1019
mempool_free_slab() 449 mq_timedreceive() 1019, 1020
mempoolt 449 mq_timedsend() 1019, 1020
memsetQ 454 mq_unlink() 1020
memset_io() 700 mqueue 619
migration 384, 387 mqueueinodeinfo 1020
mincore() 594 mremap() 463, 868
MINIX 589, 936 MS_BIND 627
MINOR 690 MSDIRSYNC 627
mips 36 MS_MANDLOCK 627
mk_pte 104, 456 MS_MOVE 628
mk_pte_huge() 102 MS_NOATIME 627
MKDEV 690 MSNODEV 627
mkdir() 593, 636 MS_NODIRATIME 627
mke2fs 954 MS_NOEXEC 627
mkfifo() 996 MS_NOSUID 627
mknod() 634, 688, 996 MS_RDONLY 627
mm_alloc() 468 MS_REC 628
mm_release() 514 MS_REMOUNT 627
mmstruct 467, 1034 MS_S YNCHRONIZE 964
mmap 1015 MS_SYNCHRONOUS 627
mmap() 56, 463, 481, 594, 804, 837, 846 MS_VERBOSE 628
mmap_sem 497 MS-DOS 588, 589, 613, 1054, 1071
mmdrop() 468 msgmsg 1012
mmput() 468 msgmsgseg 1012
MMU 72 msg_queue 1011
MMX 161 msgrecei ver 1012
mod_timer() 330, 331, 799 msgctl() 1003
modify_ldt() 83 MSGGET 1004
modprobe 1071 msgget() 61, 999, 1004
module 1063 msgrcv() 61, 1003, 1010
moduleuse 1068 msgsnd() 61, 1003, 1010
monotonic_clock 315 msync() 594, 844
more 984 msync_interval() 844
mount() 593, 627, 1071 munmap() 464, 594, 840
mount_root() 634
move_tasks() 384 кл
mpage_bio_submit() 834
mpage_end_io_read() 814 nameidata 638, 640, 646
mpage_end_io_write() 834 nanosleep() 335, 579
mpage_readpage() 812 NCP 5 89
mpage_writepage() 834 ndelay() 337
mq_close() 1020 NET_RX_SOFTIRQ 23 8
mq_getsetattr() 1020 NET_TX_SOFTIRQ 23 8
mq_notify() 1019, 1020 new_inode() 960
mq_open() 1019, 1020 NEXTSTEP 589
NFS 589, 632 D
nice() 347, 355, 385, 1028
Noop 741 РАЕ 90, 406, 478
nopage 472, 505, 841, 1017 Page Size 86
NRIRQS 225 page_add_anon_rmap() 931
NR_OPEN 617 page_address() 408
NRsyscalls 522 pageaddresshtable 408
NTFS 587, 589 pageaddressmap 408
NTP 340 page_cache_read() 843
NUMA 33, 379, 395 page_cache_readahead() 820
NW 95 page_count() 394
NX 478 page_is_buddy() 418
PAGE_MASK 99
n PAGE_OFFSET 111
U page_referenced() 878, 879, 901
О APPEND 650 page_referenced_anon() 879
О CREAT 650 page_referenced_file() 879
O~DIRECT 650 page_referenced_one() 879
oIdirectory 650 paSe"sSe 99"
О EXCL 650 FA -, , , лол
(TLARGEFILE 650 pagejable lock 930
O"NDELAY 650 page_wnteback mit() 799
CTNOATIME 650 page zone() 400
O~NOCTTY 650 PageAnon() 864
О NOFOT lowfi^O page0Ut() 892' 92?' 1016
Q-N^NRmrK 650 pagetable_init() 113
O_NONBLOCK 650 pagevec 877
O_RDONLY650 PageXvz 394
O_*DWR650 2ShSll3
°-SYNC65° parisc36
O_trUNC650 PATH 1022, 1046
O_WRONLY 650 path_lookup() 635, 637, 1047
Offset 84, 87, 89, 99 th reiease() 629, 638, 649, 1047
oldfstat() 593 pause 275
oldlstat() 593 PCD 86? 96
oldstat() 593 PCI 216, 668
oom_killjprocess() 900 pci_alloc_consistent() 704
open 471, 593, 618, 636, 649, 655, 692, 759, pci_dma_sync_single_for_cpu() 705
851, 948, 996 pci_dma_sync_single_for_device() 705
open_bdev_excl() 631 pcidriver 693
open_namei() 651 pci_free_consistent() 704
open_softirq() 240 pci_mapjpage() 705
oprofile 327 pci_map_single() 704
OS/2 589 pci_register_driver() 693
out 669 pci_set_dma_mask() 703
outofmemoryO 426, 874, 900 pci_unmap_page() 705
outb() 311, 670 pci_unmap_single() 704
outb_p() 311, 670 pclose() 984
PCMCIA 674 pmd_bad 101
pdflush 180, 792 pmd_clear 100
pdflush_operation() 794, 799 pmd_index() 104
pdflush_work 793, 794 pmd_large() 101
PDPT 91 PMD_MASK 99
per_cpu(name, cpu) 267 pmdnone 100
per_cpu_pages 420 pmd_offset() 104, 868
per_cpu_ptr(pointer, cpu) 267 pmd_page() 104
personality() 1044 pmd_present 101
PFMEMALLOC 425 PMD_SHIFT 99
PFMEMDIE 425 PMD_SIZE 99
PF_USED_MATH 163 pmd_val 100
pfn_to_page() 393 PMT 313
PFRA861 poll() 310, 593
PG_slab 435 popen() 984
PG_xyz 394 populate 472, 847
pgd_alloc() 106, 514 POSIX 30, 46, 61, 129, 342, 520, 548, 551,
pgd_bad 101 655,850, 993, 996, 1018
pgd_clear 100 POSIX_FADV_NORMAL 819
pgd_free() 106 POSIX_FADV_RANDOM 819
pgd_index() 103 POSIX_F ADVSEQUENTIAL 819
pgdnone 100 posix_fadvise() 819
pgd_offset() 103, 868 POST 108, 1054
pgdjpage() 104 ppc 36
pgd_present 101 PPP 706
pgd_val 100 pread64() 594
PGDIR_MASK 100 PREEMPT_ACTIVE 256
PGDIR_SHIFT 100 preempt_count 239? 262
PGDIR_SIZE 100 preempt_disable() 262, 275
pgoff_to_pte() 105 preempt_enable() 262, 275
pgprotval 100 preempt_enable_no_resched() 262
PIC-объект 220 preempt_schedule() 262
pid 140 preempt_schedule_irq() 256, 303
PID 128 prefetch 375
pidhashfn 139 prepare_binprm() 1047
pipe() 983, 989 preparejnamespace() 634
pipe_buffer 987 prepare_to_wait() 146, 897, 992, 994
pipeinodeinfo 985, 989, 994 prepare_to_wait_exclusive() 146
pipe_read() 990, 998 preparewrite 827, 828, 977
pipe_read_release() 990 Present 85, 412
pipe_release() 990 prioarrayt 136
pipe_write() 993, 998 prio_tree_node 872
pipe_write_release() 990 probe 708
pipefs 619, 988 proc 589, 619, 676
pipfs 619 process_timeout() 336
PIT 309, 319 profile_tick() 320, 323, 326, 327
pivot_root() 593 PROT_EXEC 484
pmd_alloc() 106, 503 PROT_NONE 484
PROT_READ 484 ptrace() 1038
PROT_WRITE 484 PTRACE_CONT 1040
prune_dcache() 894 PTRACE_SINGLESTEP 1040
prune_icache() 895 PTRACES YSC ALL 1041
pt_regs 232 PTRS_PER_PGD 100
pte_alloc_kernel() 456 PTRS_PER_PMD 100
pte_alloc_map() 107, 503 PTRS_PER_PTE 100
pte_clear 100 PTRS_PER_PUD 100
pte_dirty() 102 pud_alloc() 106, 455, 503
pte_exec() 102 pudbad 101
pte_exprotect() 102 pudclear 100
pte_file 102, 504 pud_free() 106
pte_free() 107 PUDMASK 99
pte_free_kernel() 107 pudnone 100
pte_index() 104 pud_offset() 104, 868
pte_mkclean() 102 pud_page() 104
ptemkdirtyO 102 pud_present 101
pte_mkexec() 102 PUDSHIFT 99
pte_mkold() 102 PUD_SIZE 99
pte_mkread() 102 pudval 100
pte_mkwrite() 102 pull_task() 385
pte_mkyoung() 103 put_cpu() 262
pte_modify() 103 put_cpu_no_resched() 262
ptenone 100, 504 putcpuvar(name) 267
pte_offset_kernel() 105 put_device() 683
pteoffsetmap 506 putsuper 956
pte_offset_map() 105, 868, 869 put_task_struct() 185, 378
pte_page() 105 put_user() 537
pte_present 101 pwrite64() 594
pte_rdprotect() 102 PWT 86, 96
pte_read() 102
pte_same() 101 q
pte_to_pgoff() 105
pte_unmap() 506, 869, 930 queue_delayed_work() 250
pte_user() 102 queue_work() 249
pte_val 100
pte_write() 102 p
pte_wrprotect() 102 ■"*
pte_young() 102 radix_tree_delete() 775, 777
ptep_get_and_clear 101, 459 radix_tree_extend() 774
ptep_mkdirty() 103 radix_tree_insert() 774, 777, 926
ptep_set_access_flags() 103 radix_tree_lookup() 926, 932
ptep_set_wrprotect() 103 radix_tree_maxindex() 774
ptep_test_and_clear_dirty() 103 radix_tree_node 770
ptep_test_and_clear_young() 103 radix_tree_node_alloc() 774
pthread 124 radix_tree_preload_end() 774
pthread_exit() 181 radix_tree_tag_clear() 777
pthread_kill() 581 radix_tree_tag_set() 777
radix_tree_tagged() 777 register_chrdev_region() 710, 752
RAID 723 register_filesystem() 621
raise(M81 Reiser FS 588
raise_softirq() 240, 325 release 990
raise_softirq_irqoff() 246 release_task() 184
ramdisk 632 remap filejpages 594
rawreadtrylock() 278 remap_file_pages() 463, 846
rawspintrylock() 275 remountfs 956
rawwritetrylock() 279 remove 684
rbentry 480 remove_from_page_cache() 775
RCU 282 REMOVELINKS 134, 184
rcuhead 283 remove_vm_area() 457
rcu_read_lock() 282 remove_wait_queue() 145
rcu_read_unlock() 282, 283 removexattr() 594, 947
read() 49, 590, 594, 653, 655, 696, 716, 765, rename() 50, 593, 636
803, 848, 983, 990, 996 request 734
Read/Write 86, 412, 478 request_irq() 233
read_cache_page() 775 requestlist 738
readdescriptort 808 request_module() 1072
readinode 956, 997 requestqueue 731, 738, 744
read_inode_bitmap() 962, 964 resched_task() 385
readlock 278 reschedule_interrupt() 235
read_pipe_fops 990 RESCHEDULEVECTOR 235
read_seqbegin() 281 resourse 671
read_seqretry() 281,339 restart_syscall() 579
read_swap_cache_async() 917, 930, 932, restore_fpu() 165
1018 restore_sigcontext() 572, 576
readunlock 279 resume 684
readahead() 594 ret 161
readb() 699 ret_from_exception() 253
readdir() 593 ret_from_fork() 176
readl() 699 ret_from_intr() 227, 253
readlink() 593 RLIMIT_AS 149
readpage 810, 812, 814 RLIMITCORE 149
readprofile 326 RLIMITCPU 149
readv() 594 RLIMITDATA 149
readw() 699 RLIMIT_FSIZE 149
real_lookup() 642 RLIMIT_LOCKS 149
realjimer 342 RLIMIT_MEMLOCK 150
realloc() 515 RLIMIT_MSGQUEUE 150
rebalance_tick() 362, 382 RLIMIT_NOFILE 150
recalc_sigpending() 559 RLIMIT_NPROC 150
recalc_sigpending_tsk() 559 RLIMIT_RSS 150
recalc_taskjprio() 361, 366, 367 RLIMIT_SIGPENDING 150
refill_inactive_zone() 878, 880, 882 RLIMITSTACK 150, 1035
register_binfmt() 1042 rm_from_queue() 560, 565
register_blkdev() 752 rmb() 272
registerchrdevQ 710 rmdir() 593
rmmod 1070 SCHEDRR 351, 388
ROOTDEV 634 sched_rr_get_interval() 348, 389
rootmountflags 632 sched_setaffmity() 348, 387
rootfs 619, 632 sched_setparam() 348, 389
rq_for_each_bio 736, 757 sched_setscheduler() 347, 370, 388
rt_sigaction() 548, 583 sched_yield() 348, 355, 389, 884
rt_sigpending() 548 schedule() 154, 287, 303, 336, 361, 363, 369,
rt_sigprocmask() 548 370, 375, 377, 585, 695, 773, 889, 897, 918,
rt_sigqueueinfo() 548, 581, 586 992, 994
rt_sigreturn() 572 schedule_delayed_work() 898
rt_sigsuspend() 548 schedule_tail() 176
rt_sigtimedwait() 548, 586 schedule_timeout() 146, 336
RTC 308 schedulerJick() 325, 360, 361, 363, 370, 382
run_timer_softirq() 333, 335 SCSI_SOFTIRQ 238
run_workqueue() 250 search_binary_handler() 1047
runqueue 356 search_exception_tables() 540
rwsemaphore 289 security_task_alloc() 171
rw_verify_area() 653 security_task_create() 171
rwlockinit 278 security_task_setnice() 386
rwlockt 277 securityvmenoughmemoryO 487
select(K10, 593
q select_bad_process() 900, 914
select_timer(K15,319
s390 36 sem 1007
SAJNTERRUPT 234 semarray 1006
SASAMPLERANDOM 234 sem_queue 1009
SAVEALL 225 semundo 1007, 1008
save_init_fpu() 164, 165 semctl() 1003, 1005, 1008
save_v86_state() 257 semget() 61, 999, 1005
sbjock 597 semop() 1003, 1005
sbrk() 516 send_group_sig_info() 561
scancontrol 880 send_IPI_all() 236
scan_swap_map() 920, 921 send_IPI_allbutself() 236
sched_clock() 361, 366, 371 send_IPI_mask() 236
sched_domain 381 send_IPI_self() 236
sched_exec() 1047 send_sig() 560
sched_exit() 185 send_sig_info() 560, 581
SCHEDFIFO 351, 388 send_signal() 562
sched_find_first_bit() 374 sendfile() 594, 609
sched_fork() 175, 360 seqlock_init 281
sched_get_priority_max() 348, 389 seqlockt 280
sched_get_priority_min() 348, 389 setaffmity 220
schedgetaffmityO 348, 387 set_anon_super() 633
sched_getparam() 348, 388 set_bit(nr, addr) 270
sched_getscheduler() 347, 388 set_capacity() 753
schedgroup 3 81 set_fixmap() 118
SCHED_NORMAL 351,388 set_fixmap_nocache() 118
SCHED_OTHER 388 set_intr_gate() 208
set_intr_gate(n,addr) 205 shared.vmset 872
set_ioapic_affinity_irq() 223 shm 619, 1015
SET_LINKS 134 shm_mmap() 1015
set_pgd 101 shmat() 464, 1003, 1013
set_pmd 101 shmctl() 1003
set_pte 101, 456 shmdt() 464, 1003, 1013
set_pte_atomic 101 shmeminodeinfo 1018
set_pud 101 shmem_nopage() 879, 1017
set_rtc_mmss() 321 shmem_unuse() 917
set_shrinker() 893 shmem_writepage() 1016
set_system_gate() 208 shmem_zero_setup() 487
set_system_gate(n,addr) 205 shmget() 61, 999, 1013
set_system_intr_gate() 208 shmidkernel 1014
set_system_intr_gate(n,addr) 205 shrink_cache() 887, 888
set_task_gate() 208 shrink_caches() 874, 885, 886
set_task_gate(n,gdt) 206 shrinkdcache_memory() 894
set_thread_area() 82 shrink_icache_memory() 895
set_trap_gate() 208 shrink_list() 888, 889, 894, 1016
set_trap_gate(n,addr) 206 shrink_slab() 874, 885, 893
set_tsk_need_resched() 363, 367 shrink_zone() 880, 886, 887
setfacl() 947 shrink_zones() 888
setfsuid() 1025 shrinker 893
setitimer() 341 shutdown 684
SetPageXyz 394 SIGABRT 546
setpriority() 347, 355, 386 sigaction() 548, 583
setresuid() 1025 sigaddset() 558
setreuid() 1025 sigaddsetmask() 558
setrlimit() 150 SIGALRM 341, 547
settimeofday() 533, 1029 sigandsets() 559
setuid() 1025 SIGBUS 501, 546
setup() Ю57 SIGCHLD 545, 547
setup_APIC_timer() 321 SIGCONT 547
setup_arg_pages() 1049 sigdelset() 558
setup_frame() 572, 573 sigdelsetmask() 558
setup_idt() 206, 219, 1059 sigemptyset() 558
setup_IO_APIC_irqs() 222 sigfillset() 558
setup_irq() 233, 234, 319 SIGFPE 546
setup_local_APIC() 222 sighandstruct 555
setup_memory() 109 SIGHUP 546
setup__pit_timer() 311 SIGILL 546
setup_rt_frame() 572, 575 siginfoj 557
setxattr() 594, 946 siginitset() 559
sget() 631 siginitsetinv() 559
sgid 47 SIGINT 546
SGID 657 SIGIO 547
sh 36 SIGIOT 546
sh64 36 sigismember() 559
shared.priotreenode 872 SIGKILL 325, 546
sigmask() 559 smp_call_fiinction_interrupt() 236
signal() 548, 583 smp_local_timer_interrupt() 323, 326
signal_pending() 559 smp_mb() 272
signal_wake_up() 562 smp_processor_id() 158, 412
signandsets() 559 smp_rmb() 272
sigorsets() 559 smp_wmb() 272
sigpending 557 sockfs619
sigpending() 548, 583 softirq_action 239
SIGPIPE 546 soffirq_ctx 224
SIGPOLL 547 softirq_stack 224
sigprocmask() 548, 584, 585 softirq_vec, 239
SIGPROF 341, 547 Solaris 428, 589
SIGPWR 547 spare 36
sigqueue 557, 586 specific_send_sig_info() 561
SIGQUIT 546 spin_is_locked() 274
sigreturn() 571, 572 spinlock 274, 276
SIGSEGV 499, 545, 546 spin_lock() 274
SIGSTKFLT 547 spin_lock_init() 274
SIGSTOP 547 spin_lock_irqsave() 291
sigsuspend() 548, 585 spin_trylock() 274
SIGSYS 547 spin_unlock 276
SIGTERM 547 spin_unlock() 274
sigtestsetmask() 559 spin_unlock_irqrestore() 291
sigtimedwait() 586 spin_unlock_wait() 274, 513
SIGTRAP 546 spinlockt 274
SIGTSTP 547 split_vma() 489, 491
SIGTTIN547 SSE161
SIGTTOU547 SSE2 162
SIGUNUSED 547 start_kernel() 179, 1060
SIGURG 547 start_thread() 1051
SIGUSR1 546 startup 234
SIGUSR2 546 startup_32() 112, 1058
SIGVTALRM 341, 547 stat() 593, 636
sigwaitinfo() 586 state 126
SIGWINCH 547 statfs 593, 956
SIGXCPU 325, 547 sti 233, 292
SIGXFSZ 547 sticky 47
SIMD 161 stime() 1029
slab 431 STREAMS 33
slab_destroy() 434 strlen_user() 542
SLAB_DESTROY_BY_RCU 437 stts() 164
SLAB_RECLAIM_ACCOUNT 435 submit_bh() 789, 816
slab-аллокатор 427 submit_bio() 790, 814, 834, 928, 933
sleep_on() 146 subsys_get() 679
sleep_on_timeout() 146 subsys_put() 679
SMP 222, 314, 378 subsystem 679
smp_apic_timer_interrupt() 241, 322 suid 47
smpcallfunctionQ 235 SunOS 590
superblock 595 sys_munmap() 840
superoperations 597 sys_nanosleep() 335
superformat 954 sys_ni_syscall() 522
suspend 684 sys_nice() 385
SVR4 937 sys_open() 650
swap_duplicate() 910, 926 sys_pipe() 989
swapextent 905 sys_read() 653
swap_free() 922 sys_remapjfile_pages() 846
swapheader 904, 911 sys_restart_syscall() 579
swapinfostruct 906 sys_rt_sigqueueinfo() 561
swap_readpage() 932 sys_sched_get_priority_max() 389
swaptokenmm 901 sys_sched_get_priority_min() 3 89
swap_writepage() 918, 928, 1017 sys_sched_getaffinity() 387
swapin_readahead() 930 sys_sched_getparam() 388
swaplock 909 sys_sched_getscheduler() 388
swapoff() 911 sys_schedjrr_get_interval() 390
swapon() 911 sys_sched_setaffinity() 387
swapper 350, 373 sys_sched_setparam() 389
switchto 154, 261 sys_sched_setscheduler() 388
switch_to() 377 sys_setpriority() 386
swp_entry(type,offset) 909 sys_sigaction() 582
swpoffset 909 sys_signal() 583
swptype 909 sys_sigreturn() 576
symlink() 593 sys_swapoff() 913
sync() 67, 594, 800 sys_swapon() 911
sync_blockdev() 800 sys_sync() 800
sync_dirty_buffer() 962, 964, 970 sys_tgkill() 560, 582
sync_filesystems() 801 sys_tkill() 560, 581
syncfs 801 sys_umount() 635
sync_page 773 sys_write() 533, 653
sync_page_range() 825 sysctl() 882, 895
sync_supers() 799, 801 sysenter 523, 526
sys_brk() 516 sysenter_entry() 529
sys_clone() 169 sysenter_setup() 528
sys_close() 654 sysexit 523, 531
sysdelete_module() 1070 sysfs 619
sys_execve() 1022, 1046 sysfs() 593
sysflockO 660 sysfs_create_file() 681
sys_getpriority() 386 sysfs_create_link() 681
sys_gettimeofday() 338 System V 589, 937
sys_init_module() 1068 system_call() 524, 1041
sys_io_setup() 853
sys_io_submit() 854 т
sys_ipc() 1004
sys_kill() 561, 580 Table 84, 89
sys_mknod() 634 TASKJNTERACTIVE 364
sys_mount() 627, 628 TASKJNTERRUPTIBLE 244, 353, 355
sys_msync() 844 task_rq_lock() 365
task_rq_unlock() 367 timespec_tojiffies() 336
TASKRUNNING 135, 348, 356, 365 tkill() 548, 581
TASKSTOPPED 355 TLB 96, 118
taskstruct 1062 tlb_finish_mmu() 493
task_timeslice() 363 tlb_gather_mmu() 492
TASKTRACED 355 TLB-буфер 235, 376, 410
TASK_UNINTERRUPTIBLE 353, 355, 366, TLS 82, 158
368 tmpfs619,860
tasklet_disable() 246 TPR 194, 222
tasklet_disable_nosync() 246 transaction^; 976
tasklet_enable() 246 trap_init() 208, 524
tasklethead 245 truncate() 593, 948
tasklet_hi_schedule() 246 try_to_free_buffers() 784, 785
tasklethivec 245 try_to_free_pages() 425, 874, 884
tasklet_init() 246 try_to_release_page() 785, 892
tasklet_schedule() 246, 325 try_to_unmap() 864, 873, 890, 1016
TASKLETSOFTIRQ 238, 245, 246 try_to_unmap_anon() 867
TASKLET_STATE_RUN 245, 247 try_to_unmap_file() 873
TASKLET_STATE_SCHED 245, 246, 247 try_to_unmap_one() 867, 873
taskletstruct 245 try_to_unuse() 915
taskletvec 245 try_to_wake_up() 361, 365, 370, 562
test_and_change_bit(nr, addr) 270 TS 162, 164
test_and_clear_bit(nr, addr) 270 TSJJSEDFPU 163
test_and_set_bit(nr, addr) 270 TSC 309
test_bit(nr, addr) 270 TSS 152, 528
tgkill() 548, 581 tvec_bases 331
this_rq() 356 tvecroott 332
thread_info 130, 224, 226, 252, 261, 1041
threadunion 131, 224 м
TIFJRET 252
TIF_MEMDIE 252 udelay() 311,337
TIFNEEDRESCHED 252, 256 UDF 589
TIFNOTIFYRESUME 252 UFS 589
TIF_POLLING_NRFLAG 252 UID 40
TIFSIGPENDING 252 UltraSPARC 120
TIFSINGLESTEP 252 um 36
TIF_SYSCALL_AUDIT 252 UMA 396
TIF_SYSCALL_TRACE 252 umask() 593
time() 338 umount() 593, 635
timeafter 316 umount_tree() 636
timebefore 316 UnixWare 589
timebeforeeq 316 unlink() 50, 593
time_init() 234, 318, 321 unlock_kernel() 302
timer_interrupt() 319, 322 unlock_page() 773, 810, 814, 828
timernotifyO 327 unmap 994
timeropts 315 unmap_area_pmd() 458
TIMER_SOFTIRQ 238, 335 unmap_area_pte() 459
TIMESLICE_GRANULARITY 365 unmap_area_pud() 458
unmap_region() 492 VMJOREMAP 452
unmap_vm_area() 45 8 VM_LOCKED 476
unmap_vma() 491 VMMAP 452
unmap_vmas() 493 VM_MAYEXEC 476
unnamed_dev_idr 620 VMMAYREAD 476
unregister_binfmt() 1042 VM_MAYSHARE 476
unregister_filesystem() 621 VM_MAYWRITE 476
unshare_files() 1048 VM_NONLINEAR 477
unuse_process() 917 VMRANDREAD 477
up() 58, 285 VM_READ 476
up_read() 290 VMRESERVED 477
up_write() 290 VM_SEQ_READ 476
update_atime() 812 VMSH ARED 476
update j>rocess_times() 320, 323, 324, 342 VMSHM 476
update_times() 320, 323, 326 vmstruct 451
update_wall_time() 324 VMWRITE 476
update_wall_time_one_tick() 324 vma_link() 483, 488
uptime 325 vma_merge() 487
USB 588 vma_prio_tree_foreach 873
usbfs 619 vma_prio_tree_foreach() 873
uselib() 1041 vma_prio_tree_insert() 872
User/Supervisor 86, 478 vma_prio_tree_remove() 872
ustat() 593 vmalloc() 453, 913
UTC 317 vmalloc_32() 457
utime() 593 VMALLOC_END 451
VMALLOC_OFFSET 451
у VMALLOC_START 451
vmap() 457
v850 36 vsyscall-страница 528, 575
verify_area() 536 vunmap() 457
VERITAS VxFS 589
VFAT 589 yu
vfork() 166, 169,514
vfree() 457 waitevent 147
VFS 587 waiteventinterruptible 147
vfsmountlock 636 wait_for_completion() 291
virt_tojpage() 392 wait_on_page_bit() 845
VM_ACCOUNT 477 wait4() 62, 127, 184
VM_ALLOC 452 waitpid() 62, 127, 184
vmareastruct 476, 483, 487, 865, 1015 waitqueue_active() 145
VM_DENYWRITE 476 wake_sleeping_dependent() 373
VMDONTCOPY 477 wake_up 147
VMDONTEXPAND 477 wake_up_all 147
VMEXEC 476 wakeupinterruptible 147
VMEXECUTABLE 476 wake_up_interruptible_all 147
VMGROWSDOWN 476 wake_up_interruptible_nr 147
VMGROWSUP 476 wake_up_interruptible_sync 147
VM_HUGETLB 477 wake_up_interruptible_sync() 993
VMJO 476 wake_up_locked 147
wake_up_new_task() 170 'A
wakeupnr 147 j
wake_up_process() 794 • Адрес 71
wakeup_bdflush() 795, 884, 886 j 0 линейный 72
wakeup_softirqd() 240, 243 ! 0 логический 71
wb_kupdate() 794, 799 ; Адресация блоков данных 964
wb_timer_fn() 799 i Адресное пространство 40, 55, 669
Wdtn 440 ! Адресное пространство процесса 462
while_each_task_pid() 142 ; Аллокатор памяти ядра 65
Windows 587, 589 \ Анонимное отображение 505
wmb() 272 | Аппаратный сегмент 722
work_struct 249 j Арбитр памяти 72, 268
worker_thread() 250 ! Арбитраж 194
workqueue_struct 248, 249 j Аргументы командной строки 1022
write() 49, 544, 594, 653, 655, 765, 803, j Атомарная операция 57, 268
849, 977, 983, 993, 996 j Атомарный запрос на выделение памяти
writeinode 956 j 401
writejock 279 j Атрибут 681
write_pipe_fops 993 |
write_seqlock(J81,317,319 j r-
write_sequnlock() 281,317,321 ! D
write_super 956 j Базисное дерево 770, 871
writeb() 700 j Базовый квант времени 351
writeback_control 795, 832 j Барьер*
writeback_inodes(O95,799,828 j 0 оптимизации 270
writel(O00 ! 0 памяти271
wntepage893,928 ; Библиотеки 1032
wntepages 831 I 0 совместно используемые 1021, 1032
wntew(O00 л 1Л,. ;
w i 0 статические 1032
I Бит занятости 153
X ; Битовая карта 943
^ „т. * п лл^ ! 0 разрешений ввода/вывода 160
X Window System 982 ' с 4™
Q, СЛъл \ Блок720
х86 64 36 ! л _^ _о
х -2?5 ! 0 сегментации 72,78
Xeni 589 i ^ управления памятью 72
XFS 589 • ^ управления страницами 83
xtime317 i Блокировка 272
xtime_lock317 ! ° completion 290
! 0 seqlock280
у j 0 на запись 655
! 0 на чтение 655
zap_low_mappings() 114 I Ф обязательная 655
zap_other_threads() 181 ! 0 рекомендательная 655
ZONEDMA 397 ; Блочное устройство, ведущее журнал 973
ZONE_HIGHMEM 397 j Буфер 720
ZONE_NORMAL 397 j 0 канала 986
zone_watermark_ok() 423, 425 i Буферизация 714
g Дескриптор:
О блочного устройства 748
Ведение журнала 938 0 группы 943
Вектор 191,214 0 запроса 734
Взаимная блокировка 59 0 зоны 398
Взаимодействие между процессами 999 о индексный 46, 591
Виртуальная память 64 0 кэша 429
Виртуальная файловая система 587 о локального кэша 442
Виртуальное адресное пространство 64 0 несмежных областей памяти 451
Виртуальные блочные устройства 590 <> обработчика сигналов 555
Владелец страницы 765 0 объекта 438
Внешняя фрагментация 413 0 операции чтения 808
Внутренняя фрагментация 427 0 очереди запросов 754
Временное отображение 407, 411 Л памяти 4б4
Входной регистр 670 0 сса ш
Выгружаемо^ 882 0 сегмента 74
Выгрузка 902 Л ...
,л,^ 0 сигнала 554
0 страниц 1016 Л „ , „ ,-.
„ - »,о 0 смонтированной файловой системы 624
Выделение блоков данных 968 Л ™-> -юТ
о .л/ч 0 страницы 392,783
Выделение памяти 400 . г ^АС
„ 0 тасклета 245
Выделение страниц:
л - СЛ/1 0 узла 395
0 глобальное 504 . J
Л - _.. 0 участка памяти 431
0 по требованию 503 л \ ~ л о глг
о -а « iaoo ^ файла 48,616
Выполняемый файл 1022 . ^ ,__
Выполняемый формат 1041 ° ш™за задач 198
Выравнивание объектов 439 ° шлюза ловушек 199
Высокоточный таймер событий 312 ° шлюза прерываний 199
Вытеснение в ядре 260,273 Деструктор 428
Выходной регистр 670 Дефицит памяти 874, 884
Динамический компоновщик 1032, 1048,
1051
Г Динамический режим кэширования 951
Дисковые файловые системы 588
Геометрия диска 754 Дочерняя файловая система 622
Главный мост 668 Драйвер 67
Глобальная блокировка ядра 301, 609, 628 л устрОйства 683, 692
Глобальная таблица дескрипторов 74, 80, д в файле %7'
153
Глобальный каталог страниц 97 ^.
Голова буфера 720, 779 ^
Горячие зоны 326 Ждущий запрос 1009
Группа потоков 125 Жетон защиты от выгрузки 901
Группы блоков 939 Журнал 971, 972
Д 3
Дерево приоритетного поиска 870 Завершение процесса 180
Держатель блочного устройства 749 Зависимость модулей 1067
Загрузка 1053 0 общего назначения 672
Загрузки: 0 сетевой 673
0 множественные 922 Исключение 187
0 параллельные 923 0 Debug 1040
Загрузочный сектор 1055 0 авария 190
Загрузчик 1055 0 двойная ошибка 196, 209
Закупоривание устройства 740 0 исключение при операции с плавающей
Запись журнала 974 точкой SIMD 197
Запрос к блочному устройству 730 о копирование для чтения 931
Заранее определенная рабочая очередь 251 ф ловушка 190
Засорение выгрузки 901 0 маШинная проверка 197
Защищенный режим 73 0 наруШение в сегменте сопроцессора 196
Зонный аллокатор 402, 422 0 недопустимый код операЦии 196
0 недопустимый сегмент состояния
0 отступления 405 задачи 196
0 памяти 397 0 общая защита 196, 200
0 отладка 195
И 0 ошибка 190
0 ошибка в сегменте стека 196
Идентификатор: 0 ошибкаделения наноль 195
л ург] 999 '
0 ошибка обращения к странице 89, 101,
0 выгруженной страницы 909 ^9в 203 392 493 538 903
О группы 606 Ф ошибка операции с плавающей точкой
0 группы пользователей 40 196
0 пользователя 40, 606 0 ошибка при обращении к странице 840
0 процесса 128 0 переполнение 195
Имя файла 600, 613, 636 0 проверка выравнивания 197
Индекс: ф проверка границ 195
0 базисный 870 0 программное 190
0 кучи 871 0 распознаваемое процессором 189
0 размера 871 ф сеГмент отсутствует 196
0 слота 1001 0 точка останова 195
Индексный дескриптор 600, 945 ф устройство недоступно 162, 196, 208
0 создание 960
0 удаление 963 ■ *■
Инициализация 694 "*
Интервал выгрузки 905 Кадр 573
Интерполяция времени 315 Канал 981 982
Интерпретируемый скрипт 1042 ф запись 993
Интерфейс ввода/вывода 672 0 ПОлнодуплексный 983
Интерфейсная процедура 520 0 создание 989
Интерфейсы ввода/вывода: 0 ожение 990
0 графический 673 0 чтение 983? 990
0 диска 673 Карта физических адресов 107
0 заказные 672 Каталог 948
0 клавиатуры 673 0 корневой44
0 мыши 673 0 ткущий 44
Каталог страниц 84 Лицензия 1062
0 средний 105 Логический номер блока 964
Квант времени 349 Логическое удаление 938
0 базовый 351 Локальная таблица дескрипторов 82
Класс 686 Локальный контроллер APIC 192
Классы планирования 351 Локальный таймер процессора 311
Ключ IPC 999
Кольцо асинхронного ввода/вывода 853 ОД
Командный регистр прерываний 194
Компаратор 312 Математический сопроцессор 161
Компоновка программы 1033 Межпроцессные прерывания 235
Компоновщик 1031 Метаданные 972
Конструктор 428 Микропроцессоры 80^86 668
Контекст: Многопользовательская система 39
0 аппаратный 151 Модификатор зоны 405
0 асинхронного ввода/вывода 852 Модуль 32, 42, 1061
0 прерывания 237 Мост 668
Контроллер:
0 диска 674 Ц
0 устройства 674
Контроль границ буферов 746 Нелинейное отображение в память 845
Конфликт одновременного обращения 263, Несмежная область памяти:
296 Ф выделение 453
Копирование для записи 507 0 дескриптор 451
Корневая файловая система 622, 632 0 линейные адреса 451
Корневой каталог 622 0 освобождение 457
Косвенная адресация 968 Номер:
Косвенный блок 965 0 блока файла 717
Крайний срок выполнения запроса 743 о логического блока 717
Красно-черное дерево 474 Номера устройств 709
Критическая область 57, 263, 281 Нулевая страница 507
Куча 515, 1034
Кэш 93, 118,429 п
0 Шр 607 U
0 буферов 778 Область:
0 давно неиспользуемых блоков 786 о выгрузки 904
0 диска 603, 718 0 отображения к-объектов 708
0 индексного дескриптора 614 0 памяти 427, 462,470
0 локальный 441 d создание 463
0 общий 432 о флаги 476
0 страниц 764 0 планирования 380
0 элемента каталога 592, 613, 637 Облегченный процесс 124
Обновление:
П 0 времени и даты 323
0 статистики 324
Лидер группы потоков 129 Обработка:
Лифт 741 0 исключения 209, 210
О прерываний 211,212,224,235 Отображение в память:
О служебные процедуры 231 О закрытое 835
Обработчик: 0 совместно используемое 834
О исключений 296 Отслеживание выполнения 1038
0 немаскируемых прерываний 327 Отсчет 701
0 прерываний 296 Очередь:
0 прерываний по таймеру 319, 322 ° висящих сигналов 557
0 прерывания 52 ° ждущих запросов 1008
0 сигнала 546, 571 ° запросов 729, 731,754
Обратное отображение 863 ° на выполнение 135,358
Обращение к файлу: ° ожидания 143
0 асинхронный режим 804 ° сообщений 61
0 канонический режим 803 ° сообщений IPC 1009
0 режим прямого ввода/вывода 804 Ошибка обращения к странице 493
0 режим с отображением в память 804
0 синхронный режим 804 П
Общий слой работы с блочными _
плп Пакет атомарных операции 974
устройствами 717 „ л^л
Об 42Я Пароль, смена 1024
ъект Переключение аппаратного контекста
Объектно-базированное обратное . ~
отображение 863 Переключение процессов:
Объектный файл 1031 запланированное 260
Объекты 590 . , v .„
Обычный файл 948 0 форсированное 260
^ ^ /с. Переменные окружения 1022, 1030
Обязательная блокировка 655 „ _,Л
Л 1 лл Переполненная очередь запросов 739
Ограничения на ресурсы 149 Пересылка вразброс 721
Ограничитель ресурсов 1035 Переходное состояние процессов 261
^кно 4П ЛА„ Планирование 346
Окрашивание участков памяти 440 * ^ Я53
Операции: Л л,о
л г ... ^е^ v порог времени сна 368
0 индексного дескриптора 604, 956 0 среднее время сна 353
0 суперблока 597, 956 Планировщик 41
0 файловые 608, 958 Планировщик ввода/вывода 730
0 элемента каталога 612 Поддерево переполнения 871
Операционная система 37 Поддержка ядра 705
Операция: Подсистема 679
0 задерживающая 991 Поиск области памяти 473
0 не задерживающая 991 Политика планирования 345
0 отменяемая 1005 Полудуплексный канал 983
Опережающее окно 818 Порт ввода/вывода 669
Опережающее чтение 817 Порядковый номер обращения к слоту
Освобождение блока данных 969 1001
Отключение прерываний 291 Постоянное отображение 407
Отношение отображения 881 Поток 124
Отображение: 0 events 875
0 временное 407, 411 0 kswapd 875, 896
0 постоянное 407 0 pdflush 884
Поток ядра 32, 51, 177, 792 Прямой ввод/вывод 848
О ksoftirqd243 Прямой доступ к памяти:
О создание 177 0 асинхронный 701
Права: 0 синхронный 701
0 доступа 47, 475 Пул зарезервированных страничных
0 процесса 1023 кадров 401
Прерывание 52, 187 0 памяти 448
0 асинхронное 187 Путь:
0 ввода/вывода 211 0 абсолютный 44
0 маскируемое 189 0 относительный 44
0 немаскируемое 189
0 неожиданное 218 Р
0 по таймеру 212, 309, 321
0 синхронное 187 Рабочая очередь 247
Приложение с самокэшированием 847 Рабочий поток 247
Примитивный семафор 1004 Раздел на диске 727
Приоритет процесса 346, 1028 Разделение времени 346
Программа с установкой идентификатора Размер:
пользователя 1024 0 канала 987
Программа-интерпретатор 1042 0 стека 1035
Программируемый контроллер Разрешение 1031
прерываний 191 Разрушенная файловая система 597
Программируемый таймер интервалов 309 Распределение:
Профайлер 323, 326 0 динамическое 194
Процедура-стратег 730, 755 0 статическое 193
Процесс 40,123 Расширение:
0 swapper 179 0 имени файла 1042
0 активный 354 0 физических адресов 90
0 вытесняемый 41, 348 Расширенные атрибуты 946
0 группа 63 Реальный режим 73
0 дескриптор 125, 359 Регистр:
0 зомби 62 0 приоритета задачи 194
0 интерактивный 346 0 состояния устройства 670
0 отношения 137 Регистрация:
0 пакетный 347 0 к-объектов 680
0 переключение 151, 154 ° Диска 755
0 потомок 62 0 драйвера устройства 693
0 привязанный к вводу/выводу 346 Режим:
0 приоритет 346, 351, 352, 355 ° защищенный 73
0 реального времени 347, 355, 362 ° опроса 695
0 родитель 62 ° пользовательский 38
0 с истекшим квантом времени 354 ^ прерывания 696
0 0 178 ° Файла 47
0 1 179 0 ядра 38
Процессор цифрового сигнала 701 Рекомендательная блокировка 655
Процессорная шина 668 Ресурс 671
Процессорные переменные 266 ^ 1рС 999
Q След 429
Слой отображения 717
Сеанс 63 Служебная процедура системного вызова
Сегмент 72, 722 521
О данных ядра 79 Смежные группы байтов 719
О кода ядра 79 Смещение 72
О пользовательских данных 79 Совместно используемая память
О пользовательского кода 79 ввода/вывода 675
О состояния задачи 152 Совместно используемые области памяти
О стека 1033 982
Сектор 719 Совместно используемый страничный кадр
Селектор сегмента 73, 76 860
Семафор 58, 284, 982 Создание:
0 IPC 1004 0 процесса 166
0 индексного дескриптора 304 0 областей памяти 463
0 чтения/записи 289, 304 Сокет982
Сериализация 271 Сокращающая функция 893
Сетевые файловые системы 589,612 Сообщение 982
Сигнал 60 0 IPC1010
0 висящий 549 Состояние:
0 генерирование 549, 560 0 процесса 126
0 действие по умолчанию 550, 569 0 страницы 877
0 доставка 567 Специальные файловые системы 589, 618
0 задержание 551 Список:
0 игнорирование 551 0 для процессов 1008
0 обработка 548, 554, 571 0 для семафоров 1008
0 обычный 545 0 индексных дескрипторов 751
0 реального времени 548 0 неактивный 876
0 совместно используемый 552 0 областей памяти 473
0 фатальный 551 0 процессов 134
0 частный 552 0 управления доступом 947
Символьная ссылка 647, 949 Способности процесса 1026
Синхронизация 265 Ссылка:
Система: 0 жесткая 45
0 квот 599 0 символьная 45
0 многопроцессорная 314, 321 Статическое состояние 283
0 однопроцессорная 314, 318 Стек 1035
Системная шина 668 0 гибких IRQ-запросов 224
Системный вызов 51,519 0 жеСтких IRQ-запросов 224
0 вход 523, 529 0 исключений 224
0 выход 525, 530 0 ядра 130
0 номер 521 Степень неблагополучия 882
0 обработчик 521 Страница 83
0 передача параметров 532 0 анонимная 860
0 проверка параметров 534 0 буфера 778
0 служебная процедура 521 0 выгружаемая 859
0 таблица передачи 522 0 На выброс 859
Страница (прод.)\ Типы:
О неутилизируемая 859 О страниц в алгоритме PFRA 859
О отображающая 860 Ф файлов Ext2 948
О синхронизируемая 859 0 файловых систем 618
Страничный кадр 84, 391 Типы объектов:
О выделение 420, 422 ° индексный дескриптор 591
О запрос 403 ° суперблок 591
0 кэш 419 ^ файловый объект 591
0 освобождение 405, 421, 426 t элемент каталога 591
0 состояние 394 Торвальдс, Линус 29
Страничный слот 904 0°™да ^
Структура данных 594 л г~~
r^ J със ллл ъел v монтирования 622
Суперблок 595, 940, 951 Транзакция 975
Суперпользователь 40 Транспарентная работа с файлами 938
Схема:
0 областей памяти 1034 ж,
0 слежения 327 ^
Счетчик 312 Удаление процесса 183
0 вытеснений 262 Узел 395
0 отметок времени 309 Указатель:
0 ссылок 301 0 на дескриптор процесса 128
Счетчик обращений 393, 636, 1066 0 файла 48, 607
Уничтожение процесса 180
-у Управление пространством диска 959
Управляющий блок файла 591
Таблица* Управляющий поток ядра 54, 202
0 исключений 539 Управляющий регистр устройства 669
Уровень привилегии 200
0 передачи системных вызовов 522 Усовершенствованный программируемый
Таблица Страниц 407, 469, 478, 503, 507 контроллер прерываний 192
Таблица указателей на каталог страниц 91 Устройство ввода/вывода 668
Таблицы символов ядра 1067 Утилизация страничных кадров 858
Тайм-аут 328, 335 Участок памяти 429
Таймер 327
0 POSIX342 ф
0 динамический 328
0 интервалов 328, 341 Файл:
0 процессора 311 0 закрытие 50
0 событий 312 0 открытие 48
0 управления питанием 313 О переименование 50
Такт 310 0 удаление 50
Тасклет 237,245 Файловая система 33, 43
Текущее окно 818 0 bdev 808, 814
Тенденция к выгрузке 881 0 binfmt_misc 1043
Тип отображения: Ф Ext2 828, 831
0 когерентное 703 0 Ext3 812
0 потоковое 703 ° mqueue 1020
0 pipfs 619 L|
0 sysfs676
О tmpfs 860 Частота процессора 309
0 дочерняя 622 Часы:
0 родительская 622 0 POSIX342
Файловые системы: 0 реального времени 308
0 с журналом 599
0 сетевые 599 -. -
Файловый объект 605 Ш
Файлы устройств: ттт ^£О
л г- sc^ Шина 668
0 блочные 687 Л , ГГ€%
0 символьные 687 J ввода/вывода 668
Физический адрес 72 ° заднего плана 668
Физический сегмент 722 Шлюз:
Фоновый порог 795 ° задачи 205
Фрагментация: 0 ловушки 205
0 блоков 938 0 прерывания 205
0 внутренняя 427 Ф системного прерывания 205
0 файла 959 Ф системы 205
Фрейм-буфер 673
Функции обратного вызова 794 ^
Функция: ^
0 softirq 292 Эксклюзивный процесс 144
0 допускающая задержку 236, 242, 292 Электронные схемы:
0 обратного вызова 283 0 таймеров 308
0 часов 308
X Элемент каталога 610, 949
г ^о Эмуляция 1043
Хеш-таблица 138
ХММ 162
Я
Ц Ядро 37
Цель 968
Цепной список 139
От портов ввода/eeieoda
до управления процессами
Ядро
LINUX
O'REILLV
Д. БОВЕТ, М. ЧЕЗАТИ
ОТ ПОРТОВ ВВОДА/ВЫВОДА ДО УПРАВЛЕНИЯ ПРОЦЕССАМИ
Ядро LINUX
Чтобы досконально разобраться, как работает операционная система Linux и почему она работает та
хорошо на самых разных платформах, необходимо заглянуть в самую сердцевину ядра. Ядро являете:
посредником между процессором и внешним миром, и оно решает, каким программам будет выделен
процессорное время и в каком порядке. Ядро управляет памятью так, что в системе могут одновреме
но работать сотни процессов, рационально организует пересылку данных, так что процессору не
приходится простаивать в ожидании относительно медленных жестких дисков.
Книга будет вашим гидом по важнейшим структурам данных, алгоритмам и приемам програм-
программирования, используемым в ядре. Книга содержит информацию о том, что именно происходит
внутри системы, в ней обсуждаются важные функциональные особенности ядра, специфичные
для архитектуры Intel, дается построчный комментарий приводимых фрагментов кода, приводятся
объяснения теоретических основ работы Linux.
Рассматривается версия 2.6, претерпевшая ряд важных изменений практически в каждой из
подсистем ядра, особенно в части управления памятью и блочными устройствами.
Темы, рассматриваемые в книге:
• управление памятью, в том числе буферизация файлов, выгрузка процессов
и прямой доступ к памяти (DMA);
• виртуальная файловая система и файловые системы Ext2 и Ext3;
• создание процессов и планирование их выполнения;
• сигналы, прерывания и важнейшие интерфейсы драйверов устройств;
• хронометрирование;
• синхронизация внутри ядра;
• межпроцессорное взаимодействие (IPC);
• выполнение программ.
Книга познакомит вас с внутренним устройством Linux и позволит максимально эффективно
работать с данной операционной системой. Вы узнаете, какие условия обеспечивают оптималь-
оптимальную производительность ОС Linux, как достигается минимальное время отклика системы при
планировании процессов, обращении к файлам и управлении памятью в самых разных окружениях.
БХВ-ПЕТЕРБУРГ
194354,
Санкт-Петербург
ул. Есенина, 5Б
E-mail: mail@bhv.ru
Internet: www.bhv.ru
Тел./факс: (812) 591-6243
O'REILLY*
ISBN 978-5-94157-957-0