Руководство системного программиста, часть 1
Ядро ОС Linux.
====================================================================
Руководство системного программиста
.
– 2 -
Содержание
Введение. 5
Глава 1 6
1.1 Типографские соглашения. 6
1.2 Необходимые знания для изучения книги. 7
1.3 Наставление читателю. 7
Глава 2. 8
2.2.1 Пример – vgalib. 11
2.2.2 Пример : Преобразование мыши. 13
2.3 Основы драйверов устройств. 13
2.3.1 Область имени (именная область). 14
2.3.2 Выделение памяти. 14
2.3.3 Символьные и блочные устройства. 15
2.3.4. Прерывание или поочередное опрашивание устройств ? 16
2.3.5. Механизмы замораживания и активизации. 18
2.3.5.1.Усложненный механизм заморозки. 20
2.3.6. VFS. 20
2.3.6.1. Функция lseek(). 21
2.3.6.2. Функции read() и write(). 22
2.3.6.3 Функция readdir(). 23
2.3.6.4 Функция select(). 23
2.3.6.5 Функция ioctl(). 24
2.3.6.6.Функция mmap(). 26
2.3.6.7. Функции open() и release(). 26
2.3.6.8 Функция init(). 27
2.4 Cимвольные устройства. 28
2.4.1. Инициализация. 28
2.4.2 Прерывания или последовательный вызов ? 29
2.5 Дpайвеpы для блочных устpойств. 32
2.5.1 Инициализация 32
2.5.1.1 Файл blk.h 33
2.5.1.2. Опознание комплектующих PS. 34
2.5.2. Механизм кеширования буфера. 34
2.5.3. Strategy Routine. 35
2.6. Функции поддержки. 36
2.7. Написание драйвера SCSI. 52
2.7.1. Зачем нужны драйверы SCSI. 52
2.7.2. Что такое SCSI ? 52
2.7.2.1. Термины SCSI. 53
– 3 -
2.7.3. Команды SCSI. 56
2.7.4. С чего начинать ? 58
2.7.5. Введение: сбор инструментов. 58
2.7.6. Интерфейс SCSI в Linux. 59
2.7.6. Структура Scsi_Host. 59
2.7.7.1. Переменные в структуре Scsi_Host. 60
2.7.7.1.1. name 61
2.7.7.1.2. can_queue 61
2.7.7.1.3. this_id 61
2.7.7.1.4. sg_tablesize 61
2.7.7.1.5. cmd_per_lun 62
2.7.7.1.6. present 62
2.7.7.1.7. unchecked_isa_dma 63
2.7.7.2. Функции структуры Scsi_Host. 63
2.7.7.2.1. detect() 63
2.7.7.2.1.1. Запрос IRQ. 65
2.7.7.2.2. Запрос канала DMA. 67
2.7.7.2.3. info() 67
2.7.7.2.4. queuecommand() 67
2.7.7.2.5. done() 68
2.7.7.2.6 command() 69
2.7.7.2.7 abort() 70
2.7.7.2.8 reset() 71
2.7.7.2.9 slave_attach() 71
2.7.7.2.10 bios_param() 71
2.7.8 Структура Scsi_Cmnd 72
2.7.8.1 Зарезервированная область 73
2.7.8.1.1 Информационные переменные. 73
2.7.8.1.2 Список Разветвления – компановки. (Scatter-gather) 73
2.7.8.2. Рабочие области. 75
2.7.8.2.1 Указатель scsi_done(). 75
2.7.8.2.2 Указатель host_scribble 75
2.7.8.2.3 Структура Scsi_Pointer. 75
Глава 3. 76
3.1 Каталоги и файлы /proc. 77
3.2 Стpуктуpа файловой системы /proc. 84
3.3 Пpогpамиpование файловой системы /proc. 85
Глава 4. 95
– 4 -
4.1 Исходный текст. 96
Глава 5. 99
5.1 Что поддеpживет 386 пpоцессоp? 100
5.2 Как Linux использует пpеpывания и исключения. 101
5.3 Как Linux устанавливает вектора системных вызовов. 103
5.4 Как установить свой собственный системный вызов. 104
Глава 6 105
6.1 Введение 105
6.2 Физическая память 108
6.3 Память пользовательского процесса 109
6.4 Данные управления памятью в таблице процессов 111
6.5 Инициализация памяти 112
6.5.1. Процессы и программа управления памятью 114
6.6. Выделение и освобождение памяти: политика страничной 116
6.7 Программы контроля корректности использования страниц 119
6.8. Листание (paging) 120
6.9 Управление памятью в 80386 123
6.9.1 Страничная организация (paging) в 386 123
6.9.2 Сегменты в 80386 125
6.9.3 Селекторы в 80386 128
6.9.4 Дескрипторы сегментов 130
6.9.5 Макросы, используемые при установке дескрипторов 132
Приложение А 133
Приложение B. 139
.
– 5 -
Введение.
Эта книга вдохновляет вас, начинающих исследователей ядер, не
достаточно знающих UNIX-системы, для изучения ядра Linux, когда
она впервые появилась у вас и еще тяжела для полного понимания.
Это пособие создано для того, чтобы помочь вам быстрее изучить
основные концепции и выделить из внутренней структуры Linux то,
что может понадобиться вам, чтобы, не читая полностью исходный
текст ядра, определить, что же случилось с какой-либо конкретной
переменной. Почему Linux ? Linux – это первая свободно доступная
система типа UNIX для 386 компьютеров. Она была полностью
переписана в уменьшенном объеме так, не имеет большого количества
функций, работающих с режимом реального времени, как в других
операционных системах (386BSD), и, следовательно, проста в
понимании и доступна для изменений.
UNIX появился около 20 лет назад, но только недавно появились
столь мощные микрокомпьютеры, способные поддерживать работу
операционных систем с многозадачным, многопользовательским
защищенным режимом. Кроме того, описания UNIX труднодоступны, лишь
документация о внутренностях ядра распространялась свободно. UNIX,
кажущийся в начале простым, со временем увеличивался в размерах и
превратился в объемную систему, понятную лишь профессионалу. С
Linux, однако, мы можем решить часть описанных выше проблем в
связи с тем, что:
– У Linux довольно простое ядро с хорошо структурированным
интерфейсом;
– Контроль за написанием ядра вел один человек – Linus
Torvalds, что не позволило появиться в ядре раздробленным
участкам;
– Исходные тексты ядра свободно распространяются, так что начинающие
программисты могут свободно понимать и изучать их, становясь выше в
собственных глазах.
Мы надеемся, что эта книга поможет начинающим исследователям ядер
– 6 -
разобраться в ядре Linux, поняв его структуру.
Сведения об авторских правах.
Авторские права на главу «Распределение памяти в Linux»
принадлежат Krichna Balasubramanian. Некоторые изменения
запатентованы Майклом К.Джонсоном и Дугласом Р.Джонсоном.
» Как система вызывает процедуру «: авторскими правами на
оригинал этой статьи обладает Stanley Scalsky.
«Написание драйвера устройства SCSI»: авторскими правами
обладает Ric Faith.
Глава 1
» Прежде чем вы начали…»
1.1 Типографские соглашения.
– Выделенный шрифт используется для обозначения определений,
предупреждений и ключевых слов в языке.
– Курсив используется для обозначения вставок и введений для
новых статей.
– Наклонный шрифт используется для обозначения
мета-переменных в тексте, особенно в командной строке:
ls – l [foo]
где [foo] – имя файла,как /bin/cp.Иногда довольно сложно в
тексте заметить наклонный шрифт, и соответствующее выражение
берется в [---].
– Шрифт печатной машинки используется для отображения
ответной информации компьютера:
ls -l /bin/cp
[-rwxr-xr-x 1 root wheel 12104 Sep 25 15:53 /bin/cp]
– 7 -
также он используется для примеров в кодах Си для обозначения
системных команд и для описания конфигурационных файлов. Иногда
для наглядности эти примеры помещаются в рамку.
– В
<—>
скобки берется нажатая клавиша:
Для продолжения нажмите
.
– Звездочка на полях выделяет место, требующее особого внимания.
1.2 Необходимые знания для изучения книги.
Для того, чтобы понять эту книгу, вы должны хотя бы
поверхностно знать Си. Это означает, что вы можете читать
программы на Си, не уделяя внимания справочникам. Вы должны уметь
писать простейшие программы на Си и понимать структуры, указатели
и макросы, атак же прототипы ANSI C.
Вы должны также представлять тексты стандартной библиотеки
ввода/вывода, так как стандартные библиотеки не доступны в ядре.
Некоторые часто используемые функции ввода/вывода были переписаны
внутри ядра и они по возможности описываются далее. Также вам
нужен хороший текстовый редактор, перекомпилирование ядра Linux и
умение выполнять простейшие задачи системного администратора,
такие как включение информации в /dev/.
1.3 Наставление читателю.
В этой части приводятся некоторые полезные при прочтении вещи.
Статические переменные.
Всегда определяйте статические переменные. Масса почти
случайных ошибок возникает из – за игнорирования статических
переменных. Т.к. ядро на самом деле не является запускаемой
программой, сегмент bss не всегда обнуляется в зависимости от
метода загрузки.
Невозможность использования libc.
Библиотека libc не доступна в ядре, однако некоторые функции
из нее были продублированы. Смотрите разделы книги, в которых эти
функции документированы. В основном, это разделы 3 и 9.
/\ Linux – это не UNIX.
\/ Linux – система, написанная не для коммерческого распространения.
Глава 2.
Драйверы устройств.
Что такое драйвер устройства.
Создание драйвера устройства – дело достаточно трудоемкое.
Запись на жесткий диск требует помещения определенных цифровых
данных в определенное место, ожидания ответа на запрос о
готовности жесткого диска, затем аккуратной пересылки информации.
Запись на флопповод проходит еще сложнее – нужен постоянный
контроль на текущим состоянием дискеты.
Вместо помещения кода каждого отдельного приложения
управляющего устройством, вы разделяете код между приложениями.
Вам следует защитить этот код от других пользователей и
использующих его программ.
Если вы верно сделали это, то вы можете без смены приложений
подключать или убирать устройства. Более того, вы должны иметь
возможности ОС – загрузить вашу программу в память и запустить ее.
Так что ОС, в сущности, – это набор привилегированных, общих и
частных функций или функций аппаратного обеспечения низкого
уровня,функций работы с памятью и функций контроля.
Все версии UNIX имеет абстрактный способ считывания и записи
– 9 -
на устройство. Действующие устройства представляются в виде
файлов, так что одинаковые вызовы ( read(), write() и т.п.) могут
быть использованы и как устройства и как файлы.
Внутри ядра существует набор функций, отмеченных как файлы,
вызываемые при запросе для ввода/вывода на файлы устройств, каждый
из которых представляет свое устройство.
Всем устройствам, контролируемым одним драйвером, дается один
и тот же основной номер, и различные подномера.
Эта глава описывает, как написать любой из допускаемых в
Linux типов драйверов устройств : символьных, блочных, сетевых и
драйверов SCSI. Она описывает, какие функции вы должны написать,
как инициализировать драйверы и эффективно выделять под них
память, какие функции встроены в Linux для упрощения деятельности
такого рода.
Создание драйвера устройств для Linux оказывается более
простым чем мнится на первый взгляд, ибо оно включает в себя
написание новой функции и определение ее в системе переключения
файлов(VFS).
Тем самым, когда доступно устройство, присущее вашему
драйверу, VFS вызывает вашу функцию.
Однако, вы должны помнить, что драйвер устройства является
частью ядра. Это означает, что ваш драйвер запускается на уровне
ядра и обладает большими возможностями : записать в любую область
памяти, повредить ваш монитор или разбить вам унитаз в случае,
если ваш компьютер управляет сливным баком.
Также ваш драйвер будет запущен в режиме работы с ядром, а
ядро Linux, как и большинство ядер UNIX, не имеет средств
принудительного сброса. Это означает, что если ваш драйвер будет
долго работать, не давая при этом работать другим программам, ваш
компьютер может «зависнуть «. Нормальный пользовательский режим с
последовательным запуском не обращается к вашему драйверу.
– 10 -
Если вы решили написать драйвер устройства, вы должны
внимательно прочитать всю эту главу, однако, нет гарантий, что эта
глава не содержит ошибок, и вы не сломаете ваш компьютер, даже
если будете следовать всем инструкциям. Единственный совет -
сохраняйте информацию перед запуском драйвера.
Драйверы пользовательского уровня.
Не всегда нужно писать драйвер для устройства, особенно если
за устройством следит всего одно приложение. Наиболее полезным
примером этому является устройство карты памяти, однако вы можете
сделать карту памяти с помощью устройств ввода/вывода (доступ к
устройствам осуществляется с помощью функций inpb() и outpb()).
Если вы работаете в режиме superuser, вы можете использовать
функцию mmap для того, чтобы поместить вашу функцию в какую-то
область памяти. С помощью этой процедуры вы сможете весьма просто
работать с адресами памяти, как с обычными переменными.
Если ваш драйвер использует прерывание, то вам придется
работать внутри ядра, так как не существует других путей для
прерываний обычных пользовательских процессов. В проекте DOSEMU
однако, есть Простейший Генератор прерываний – SIG, но он работает
недостаточно быстро, как это можно было ожидать от последней
версии DOSEMU.
Прерывание – это жестко определенная процедура. Также вы при
установке своего аппаратного обеспечения вы определяете линию
IRQ для физического сигнала прерываний, возникающего, когда
устройство обращается к драйверу. Это происходит, когда
устройство пересылает или запрашивает информацию, а также при
обнаружении каких-либо исключительных ситуаций, о которых должен
знать драйвер. Для обработки прерываний в ядре и для обработки
сигналов на пользовательском уровне используется одна и та же
структура данных – sigaction. Таким образом, где сигналы
аппаратных прерываний доставляются ядру точно так же, как
– 11 -
системные сигналы на уровне пользовательского обеспечения.
Если ваш драйвер должен обращаться к нескольким процессам
сразу или управлять общими ресурсами, тогда вы должны написать
драйвер устройства, и драйвер пользовательского уровня вам не
подходит.
2.2.1 Пример – vgalib.
Хорошим примером драйвера пользовательского уровня является
библиотека vgalib. Стандартные функции read() и write() не
подходят для написания действительно быстрого графического
драйвера, и поэтому существует библиотека функций, которая
концептуально работает как драйвер устройства, но на
пользовательском уровне. Все функции, которые используют ее,
должны запускать setuid, так как она использует системную функцию
ioperm(). Функции, которые не запускают setuid, обладают
возможностью записи в /DEV/MEM, если у вас есть группы mem или
kmem, которые позволяют это, но только корневые процессы могут
запускать ioperm().
Есть несколько портов ввода/вывода, относящихся к графике
VGA. Vgalib дает им символические имена с помощью #define, и далее
используют ioperm() для разрешения функции правильного прочтения и
записи в эти порты.
if (ioperm(CRT_IC, 1, 1)) {
printf(«VGAlib: can’t get I/O permission \n»);
exit(-1);
}
ioperm(CRT_IM, 1, 1);
ioperm(ATT_IW, 1, 1);
[--]
Это требует лишь однократной проверки, так как единственной
причиной нефункционирования ioperm() может быть обращение к ней не
в статусе superuser или во время смены статуса.
– 12 -
/\
\/ После вызова этой функции разрешается использование inb и outb
инструкций, однако лишь с определенными портами. Эти инструкции
могут быть доступны без использования прямого ассемблерного кода
, но работают они лишь в случае компиляции с параметром
optimization on и с ключом -0?. Для более подробных сведений читай
.
После обращения в порты ввода вывода vgalib засылает информацию
в область ядра следующим образом :
/* open /dev/mem */
if ((mem_fd = open(«/dev/mem», 0_RDWR) )
< 0) {
prntf( «VGAlib: can’ t open /dev/mem \n»);
exit (-1);
}
/* mmap graphics memory */
if ((graph_mem = malloc(GRAPH*SIZE + (PAGE-SIZE-1))) == NULL) {
printf( » VGAlib: allocation error \n «);
exit (-1);
}
if ((unsigned long)graph_mem % PAGE_SIZE)
graph_mem += PAGE_SIZE – ((unsigned long)graph_mem % PAGE_SIZE);
graph_mem = (unsigned char *)mmap(
(caddr_t)graph_mem,
GRAPH_SIZE,
PROT_READ|PROT_WRITE,
MAP_SHARED|MAP_FIXED,
mem_fd,
GRAPH_BASE
);
if ((long)graph_mem
< 0) { printf(» VGAlib: mmap error \n»); exit (-1); } – 13 – В начале программа открывает /dev/mem, затем выделят достаточное количество памяти для распределения на страницу, затем меняет карту памяти. GRAPHSIZE – размер памяти vga. GRAPHBASE – адрес начала памяти VGA в /dev/mem. Затем, записывая в адрес возвращаемый mmap(), программа осуществляет запись в память экрана. 2.2.2 Пример : Преобразование мыши. Если вы хотите написать драйвер, работающий так же, как и драйвер на уровне ядра, но не находящийся в его области, то вы можете создать fifo (буфер – first in, first out). Обычно он помещается в директорию /dev (во время нефункционирования) и ведет себя как подключенное устройство. В частности, это используется когда вы используете мышь типа PS/2 и хотите запустить XFree86. Вы должны создать fifo, называемый /dev/mouse, и запустить программу mconv, которая, читая сигналы мыши PS/2 из /dec/psaux, пишет эквивалентные сигналы microsoft mouse в /dev/mouse. В этом случае XFree86 будет читать сигналы из /dev/mouse и функционировать также как и при подключенной microsoft mouse. 2.3 Основы драйверов устройств. Мы будем полагать, что вы не хотите писать драйвер на пользовательском уровне, а желаете работать непосредственно в области ядра. В таком случае вам придется иметь дело с файлами .с и .h. Мы будем условно обозначать ваши труды как foo.c и foo.h. – 14 – 2.3.1 Область имени (именная область). Первое что вы должны сделать при написании драйвера – назвать устройство. Имя должно выть кратким – строка из двух – трех символов. К примеру, параллельные устройства – «lp», дисководы «fd», диски SCSI – «sd». Создавая ваш драйвер, называйте функции в нем с первыми тремя буквами избранной строки в имени. Так как мы называем его foo – функции в нем соответственно – foo_read и foo_write. 2.3.2 Выделение памяти. Выделение памяти в ядре отличается от выделения памяти на пользовательском уровне. Вместо функции malloc() выделяющее почти неограниченное количество памяти, существует kmalloc(), которая имеет некоторые отличия: – Память выделяется кусками размером степени 2, за исключением кусков больше 128 байтов, размер коих равен степени 2 за вычетом части под метку о размере. Вы можете запросить произвольный размер, однако это будет неэффективно, так как 31 байтового об’екта, к примеру, выделяется 32 байтовый кусок. Общий предел выделяемой памяти 131056 байт. – В качестве второго аргумента kmalloc() использует приоритет. Он используется в качестве аргумента функции get_free_page(), где он используется в качестве числа определяющего момент возврата. Обычно используемый приоритет – GFP_KERNEL. Если функция может быть вызвана с помощью прерывания используйте GFP_ATOMIC и приготовьтесь к тому, что функция может не работать. Это происходит из-за того, что при использовании GFP_KERNEL kmalloc() может не быть активным в любой момент времени, что не возможно при прерывании. Можно так же использовать опцию GFP_BUFFER, которая используется для выделения ядром области буфера. В драйверах устройств она не используется. – 15 – Для очистки памяти, выделенной kmalloc(), используйте функции kfree() и kfree_s(). Они также несколько отличаются от функции free() : – kfree() – это макрос, вызывающий kfree_s() и работающая как free() вне ядра. – Если вы знаете размеры об’екта, удаляемого из памяти, вы можете ускорить процесс, запуская сразу kfree_s(). У него существуют два аргумента : первым является аргумент kfree(), вторым – размер удаляемого об’екта. См 2.6 для получения более подробной информации о kmalloc(), kfree() и о других полезных функциях. Другой способ сохранить память – выделение ее во время инициализации. Ваша инициализационная функция foo_init() в качестве аргумента использует указатель на текущий конец памяти.Она может взять столько памяти, сколько хочет сохранить указатель/указатели на эту память и возвратить указатель на новый конец памяти.Преимуществом этого метода является то, что при выделении большого буфера в случае, если foo – драйвер не находит foo- устройства, подключенного к компьютеру, память не тратится. Функция инициализации подробно обсуждается в части 2.3.6. Будьте предельно аккуратны при использовании kmalloc(), используйте его только в случае крайней необходимости. Помните, что память в ядре не своппится. Аккуратно выделяйте ее, а затем каждый раз очищайте ее функцией frее(). ! Существует возможность выделения виртуальной памяти с помощью vmalloc(), однако это будет описано лишь в главе VMM во вре]
]>



