Copyright © 2000, 2001, 2002, 2003, 2004, 2005 The FreeBSD Documentation Project
Добро пожаловать в Руководство по Архитектуре FreeBSD. Этот документ находится в процессе написания и представляет собой результат работы множества людей. Многие секции еще не написаны, а некоторые из написанных требуют обновления. Если Вы хотите помочь этому проекту, напишите в Список рассылки Проекта Документации FreeBSD.
Последняя версия этого документа постоянно доступна с Всемирного Веб Сайта FreeBSD. Этот документ может также быть найден в множестве форматов с FTP Сервера FreeBSD или одного из множества зеркал.
FreeBSD это зарегистрированная торговая марка The FreeBSD Foundation.
UNIX это зарегистрированная торговая марка The Open Group в США и других странах.
Sun, Sun Microsystems, SunOS, Solaris, и Java это торговые марки или зарегистрированные торговые марки Sun Microsystems, Inc. в Соединенных Штатах и других странах.
Apple и QuickTime это торговые марки Apple Computer, Inc., зарегистрированные в США и других странах.
Macromedia и Flash это торговые марки или зарегистрированные торговые марки Macromedia, Inc. в Соединенных Штатах и/или других странах.
Microsoft, Windows, и Windows Media это или зарегистрированные торговые марки или торговые марки Microsoft Corporation в Соединенных Штатах и/или других странах.
PartitionMagic это зарегистрированная торговая марка PowerQuest Corporation в Соединенных Штатах и/или других странах.
Многие из обозначений, используемых производителями и продавцами для обозначения своих продуктов, заявляются как торговые марки. Когда такие обозначения появляются в этой книге, и Проекту FreeBSD известно о торговой марке, к обозначению добавляется знак '™'.
Распространение и использование исходных (SGML DocBook) и ''скомпилированных'' форм (SGML, HTML, PDF, PostScript, RTF и прочих) с модификацией или без оной, разрешены при соблюдении следующих соглашений:
Распространяемые копии исходного кода (SGML DocBook) должны сохранять вышеупомянутые объявления copyright, этот список положений и следующий отказ от ответственности в первых строках этого файла в неизменном виде.
Распространяемые копии скомпилированных форм (преобразованные в другие DTD, конвертированные в PDF, PostScript, RTF и другие форматы) должны повторять вышеупомянутые объявления copyright, этот список положений и следующий отказ от ответственности в документации и/или других материалах, поставляемых с дистрибьюцией.
Важно: ЭТА ДОКУМЕНТАЦИЯ ПОСТАВЛЯЕТСЯ ПРОЕКТОМ ДОКУМЕНТАЦИИ FREEBSD "КАК ЕСТЬ" И ЛЮБЫЕ ЯВНЫЕ ИЛИ НЕЯВНЫЕ ГАРАНТИИ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ НЕЯВНЫМИ ГАРАНТИЯМИ, КОММЕРЧЕСКОЙ ЦЕННОСТИ И ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ ОТРИЦАЮТСЯ. НИ ПРИ КАКИХ УСЛОВИЯХ ПРОЕКТ ДОКУМЕНТИРОВАНИЯ FREEBSD НЕ НЕСЕТ ОТВЕТСТВЕННОСТИ ЗА ЛЮБОЙ ПРЯМОЙ, КОСВЕННЫЙ, СЛУЧАЙНЫЙ, СПЕЦИАЛЬНЫЙ, ОБРАЗЦОВЫЙ ИЛИ ПОСЛЕДУЮЩИЙ УЩЕРБЫ (ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ПОСТАВКОЙ ТОВАРОВ ЗАМЕНЫ ИЛИ УСЛУГ; ПОТЕРЮ ДАННЫХ ИЛИ ИХ НЕПРАВИЛЬНУЮ ПЕРЕДАЧУ ИЛИ ПОТЕРИ; ПРИОСТАНОВЛЕНИЕ БИЗНЕСА), И ТЕМ НЕ МЕНЕЕ ВЫЗВАННЫЕ И В ЛЮБОЙ ТЕОРИИ ОТВЕТСТВЕННОСТИ, НЕЗАВИСИМО ОТ КОНТРАКТНОЙ, СТРОГОЙ ОТВЕТСТВЕННОСТИ, ИЛИ ПРАВОНАРУШЕНИИ (ВКЛЮЧАЯ ХАЛАТНОСТЬ ИЛИ ИНЫМ СПОСОБОМ), ВОЗНИКШЕМ ЛЮБЫМ ПУТЕМ ПРИ ИСПОЛЬЗОВАНИИ ЭТОЙ ДОКУМЕНТАЦИИ, ДАЖЕ ЕСЛИ БЫ БЫЛО СООБЩЕНО О ВОЗМОЖНОСТИ ТАКОГО УЩЕРБА.
В этом разделе рассматривается процесс загрузки и инициализации системы начиная с BIOS (firmware) POST и заканчивая созданием первого пользовательского процесса. Начальные шаги загрузки системы сильно архитектурно зависимы, и поэтому в качестве примера будет рассматриваться архитектура IA-32.
Компьютер под управлением FreeBSD может быть загружен несколькими способами, из которых самым обычным является загрузка с жёсткого диска, на котором установлена ОС. Этот вариант мы здесь и рассмотрим. Процесс загрузки осуществляется в несколько этапов:
BIOS POST
этап boot0
этап boot2
стадия загрузчика
инициализация ядра
Стадии boot0 и boot2 являются соответственно стадиями загрузки 1 и 2, описанными в boot(8), как первые стадии трёх-этапного процесса загрузки FreeBSD. Во время прохождения каждой стадии на экран выводится различная информация, поэтому вы можете визуально определить стадию, на которой находитесь, с помощью нижеприведённой таблицы. Однако учтите, что на различных машинах сами данные могут различаться:
могут различаться |
сообщения BIOS (firmware) |
F1 FreeBSD F2 BSD F5 Disk 2 |
boot0 |
>>FreeBSD/i386 BOOT Default: 1:ad(1,a)/boot/loader boot: |
boot2a |
BTX loader 1.0 BTX version is 1.01 BIOS drive A: is disk0 BIOS drive C: is disk1 BIOS 639kB/64512kB available memory FreeBSD/i386 bootstrap loader, Revision 0.8 Console internal video/keyboard (jkh@bento.freebsd.org, Mon Nov 20 11:41:23 GMT 2000) /kernel text=0x1234 data=0x2345 syms=[0x4+0x3456] Hit [Enter] to boot immediately, or any other key for command prompt Booting [kernel] in 9 seconds..._ |
загрузчик |
Copyright (c) 1992-2002 The FreeBSD Project. Copyright (c) 1979, 1980, 1983, 1986, 1988, 1989, 1991, 1992, 1993, 1994 The Regents of the University of California. All rights reserved. FreeBSD 4.6-RC #0: Sat May 4 22:49:02 GMT 2002 devnull@kukas:/usr/obj/usr/src/sys/DEVNULL Timecounter "i8254" frequency 1193182 Hz |
ядро |
Примечания: a. Эта строка появится в том случае, если пользователь нажмёт клавишу сразу после выбора ОС на стадии boot0. |
При включении питания компьютера регистрам процессора присваиваются некоторые заранее определённые значения. Одним из регистров является регистр указателя на инструкцию (instruction pointer), и его значение после включения питания компьютера точно определено - это 32-битное значение 0xfffffff0. Регистр указателя на инструкцию указывает на код, который должен быть выполнен процессором. Ещё один регистр - 32-битный регистр управления cr1, значение которого сразу после загрузки равно 0. Один из битов cr1, а именно бит PE (Protected Enabled), указывает на то, работает ли процессор в защищённом (Protected mode) или в реальном режиме (Real mode). Поскольку во время загрузки этот бит не установлен, процессор загружается в реальном режиме. Реальный режим, помимо других вещей, определяет, что линейные и физические адреса идентичны.
Значение 0xfffffff0 немного меньше, чем 4Гб; поэтому если машинa не имеет 4Гб физической памяти, то она не сможет обратиться к существующему адресу в памяти. Аппаратное обеспечение компьютера преобразовывает этот адрес так, что он указывает на область памяти BIOS.
BIOS расшифровывается, как Basic Input Output System (Базовая Система Ввода Вывода) и представляет собой микросхему на материнской плате с относительно небольшим объёмом памяти, доступной только для чтения (ROM). В этой памяти хранятся различные низкоуровневые процедуры, являющиеся специфическими для этого аппаратного обеспечения. Итак, процессор вначале перейдёт на адрес 0xfffffff0, который на самом деле находится в области памяти BIOS. Обычно по этому адресу располагается инструкция перехода к процедурам BIOS POST.
POST расшифровывается, как Power On Self Test (Самотестирование при включении питания). Это набор процедур, включающий проверку памяти, проверку системной шины и другие низкоуровневые процедуры, при помощи которых процессор может должным образом инициализировать компьютер. Важным шагом на данной стадии является определение загрузочного устройства. Все современные BIOS позволяют устанавливать загрузочное устройство вручную, поэтому вы можете загружаться с флоппи-диска, компакт-диска, жёсткого диска и т.д.
Самое последние действие POST - это инструкция INT 0x19. Эта инструкция читает 512 байт из первого сектора загрузочного устройства и записывает их в память по адресу 0x7c00. Термин первый сектор происходит от архитектуры жёсткого диска, в котором магнитный диск разделён на цилиндрические дорожки. Дорожки пронумерованы и каждая из них разделена на сектора (обычно 64). Нулевая дорожка располагается на внешний стороне у самого края магнитного диска, и её первый сектор (дорожки или цилиндры нумеруются с нуля, но сектора - с единицы) имеет особое значение. Его также называют Главная Загрузочная Запись (Master Boot Record), или сокращённо - MBR. Оставшиеся сектора на первой дорожке никогда не используются [1].
Обратите внимание на файл /boot/boot0. Это маленький файл размером 512 байт, и именно его процедура установки FreeBSD запишет в MBR вашего жёсткого диска, если вы выбрали опцию ''bootmanager'' во время установки.
Как уже отмечалось, инструкция INT 0x19 загружает MBR, т.е. содержимое boot0, в память по адресу 0x7c00. Взгляните на файл sys/boot/i386/boot0/boot0.s, который вам поможет разобраться в том, что происходит на данной стадии. Это менеджер загрузки, написанный Робертом Нордиером (Robert Nordier).
MBR, или boot0, содержит специальную структуру - таблицу разделов (partition table), располагающуюся со смещения 0x1be. Она содержит 4 записи по 16 байт каждая, называемые записями о разделах, которые указывают, каким образом диск(и) делится на разделы, или слайсы, если пользоваться терминологией FreeBSD. Один байт из этих 16 указывает на то, является ли раздел (слайс) загрузочным, или нет. Только одна запись должна содержать этот флаг установленным, в противном случае код boot0 не будет выполняться далее.
Запись о разделе содержит следующие поля:
1 байт, определяющий тип файловой системы
1 байт для загрузочного флага
шести-байтовый дескриптор в формате CHS
восьми-байтовый дескриптор в формате LBA
Дескриптор записи о разделе хранит информацию о точном расположении раздела на диске. Оба дескриптора CHS и LBA описывают одну и ту же информацию, но каждый по-своему: LBA (Logical Block Addressing, Логическая блоковая адресация) содержит информацию о начальном секторе и длине раздела, в то время как CHS (Cylinder Head Sector, Цилиндр-Головка-Сектор) хранит координаты начального и конечного секторов раздела.
Менеджер загрузки просматривает таблицу разделов и выводит на экран меню, чтобы пользователь мог выбрать с какого диска и с какого слайса грузиться. По нажатию соответствующей клавиши boot0 выполняет следующие действия:
модифицирует загрузочный флаг выбранного раздела, делая его загрузочным и сбрасывает этот флаг с предыдущего загрузочного раздела
сохраняет себя на диск, чтобы запомнить какой раздел (слайс) был выбран для загрузки; таким образом, в следующий раз он будет разделом для загрузки по умолчанию
загружает первый сектор выбранного раздела (слайса) в память и передаёт управление туда
Какие данные должны располагаться в самом первом секторе загрузочного раздела (слайса), в нашем случае, слайса FreeBSD? Как вы уже наверное догадались, это boot2.
Вам наверное интересно, почему за boot0 следует boot2, а не boot1. На самом деле, в каталоге /boot также есть и файл boot1 размером 512 байт. Он используется для загрузки с флоппи-диска и выполняет ту же работу, что и boot0 при загрузке с жёсткого диска: он обнаруживает boot2 и передаёт ему управление.
Вы могли заметить, что также существует и файл /boot/mbr. Это упрощённая версия boot0. Код mbr не предоставляет меню для пользователя: он просто слепо загружает раздел, помеченный, как активный.
Исходные тексты boot2 находятся в каталоге sys/boot/i386/boot2/, а сам исполняемый файл в /boot. Файлы boot0 и boot2 из /boot не используются в процессе загрузки, однако используются такими утилитами, как boot0cfg. Действительное местоположение boot0 - в MBR, а boot2 - в начале загрузочного слайса FreeBSD. Эти места не находятся под контролем файловой системы, поэтому являются невидимыми для таких команд, как ls.
Основная задача boot2 состоит в загрузке файла /boot/loader, который является третьей стадией процесса загрузки.
Код из boot2 не может использовать какие-либо сервисы вроде
open()
и read()
, потому как
ядро ещё не загружено. Он должен просмотреть жёсткий диск, зная о структуре файловой
системы, найти файл /boot/loader, считать его в память с
помощью средств BIOS и затем передать управление в точку входа загрузчика.
Кроме того, boot2 с помощью командной строки позволяет пользователю загрузить загрузчик с другого диска, юнита, слайса и раздела.
Двоичный файл boot2 создаётся особым образом:
sys/boot/i386/boot2/Makefile boot2: boot2.ldr boot2.bin ${BTX}/btx/btx btxld -v -E ${ORG2} -f bin -b ${BTX}/btx/btx -l boot2.ldr \ -o boot2.ld -P 1 boot2.bin
Этот фрагмент Makefile показывает, что btxld(8) используется для компоновки двоичного файла. BTX, чьё название происходит от BooT eXtender (расширитель загрузки) является фрагментом когда, предоставляющим окружение защищённого режима для программы, называемой клиентом, c которой он скомпонован. Итак, boot2 - это BTX клиент, т.е. он использует сервисы предоставляемые BTX.
Утилита btxld является компоновщиком. Она связывает два двоичных файла. Разница между btxld(8) и ld(1) заключается в том, что ld обычно компонует объектные файлы в разделяемый объект или в исполняемый файл, в то время как btxld связывает объектный файл с BTX, создавая двоичный файл, пригодный для размещения в начале раздела для загрузки системы.
boot0 передаёт управление в точку входа BTX. После этого BTX переключает процессор в защищённый режим и подготавливает простое окружение перед вызовом клиента. Это включает в себя:
виртуальный режим v86. Означает, что BTX является v86 монитором. Инструкции реального режима, такие как pushf, popf, cli, sti доступны для клиента.
Устанавливается Таблица Дескрипторов Прерываний (Interrupt Descriptor Table, IDT). Таким образом, все аппаратные прерывания перенаправляются к стандартным процедурам BIOS, а также устанавливается прерывание 0x30 для того, чтобы быть шлюзом к системным вызовам.
Определены два системных вызова: exec
и exit
:
sys/boot/i386/btx/lib/btxsys.s: .set INT_SYS,0x30 # Interrupt number # # System call: exit # __exit: xorl %eax,%eax # BTX system int $INT_SYS # call 0x0 # # System call: exec # __exec: movl $0x1,%eax # BTX system int $INT_SYS # call 0x1
BTX создаёт Глобальную Таблицу Дескрипторов (Global Descriptors Table, GDT):
sys/boot/i386/btx/btx/btx.s: gdt: .word 0x0,0x0,0x0,0x0 # Null entry .word 0xffff,0x0,0x9a00,0xcf # SEL_SCODE .word 0xffff,0x0,0x9200,0xcf # SEL_SDATA .word 0xffff,0x0,0x9a00,0x0 # SEL_RCODE .word 0xffff,0x0,0x9200,0x0 # SEL_RDATA .word 0xffff,MEM_USR,0xfa00,0xcf# SEL_UCODE .word 0xffff,MEM_USR,0xf200,0xcf# SEL_UDATA .word _TSSLM,MEM_TSS,0x8900,0x0 # SEL_TSS
Код и данные клиента начинаются с адреса MEM_USR (0xa000), а селектор (SEL_UCODE) указывает на сегмент кода клиента. Дескриптор SEL_UCODE имеет Уровень Привилегий Дескриптора (Descriptor Privilege Level, DPL) равный 3, который является наименьшим уровнем привилегий. Однако обработчик инструкции INT 0x30 располагается в сегменте, на который указывает селектор SEL_SCODE (код супервизора), как показано в коде создания IDT:
mov $SEL_SCODE,%dh # Segment selector init.2: shr %bx # Handle this int? jnc init.3 # No mov %ax,(%di) # Set handler offset mov %dh,0x2(%di) # and selector mov %dl,0x5(%di) # Set P:DPL:type add $0x4,%ax # Next handler
Таким образом, когда клиент вызывает __exec()
код будет
исполнен с наивысшими привилегиями. Это позволяет ядру позже, при необходимости, изменять
структуры данных защищённого режима, таких как таблицы страниц, GDT, IDT и т.д.
boot2 определяет важную структуру struct bootinfo. Эта структура инициализируется кодом boot2 и передаётся загрузчику, а затем дальше ядру. Некоторые поля данной структуры устанавливаются boot2, остальные загрузчиком. Кроме всей прочей информации, эта структура содержит имя файла ядра, BIOS геометрию жёсткого диска, BIOS номер привода загрузочного устройства, доступную физическую память, указатель envp и т.д. Вот её определение:
/usr/include/machine/bootinfo.h struct bootinfo { u_int32_t bi_version; u_int32_t bi_kernelname; /* represents a char * */ u_int32_t bi_nfs_diskless; /* struct nfs_diskless * */ /* End of fields that are always present. */ #define bi_endcommon bi_n_bios_used u_int32_t bi_n_bios_used; u_int32_t bi_bios_geom[N_BIOS_GEOM]; u_int32_t bi_size; u_int8_t bi_memsizes_valid; u_int8_t bi_bios_dev; /* bootdev BIOS unit number */ u_int8_t bi_pad[2]; u_int32_t bi_basemem; u_int32_t bi_extmem; u_int32_t bi_symtab; /* struct symtab * */ u_int32_t bi_esymtab; /* struct symtab * */ /* Items below only from advanced bootloader */ u_int32_t bi_kernend; /* end of kernel space */ u_int32_t bi_envp; /* environment */ u_int32_t bi_modulep; /* preloaded modules */ };
boot2 в бесконечном цикле ожидает пользовательского ввода,
после чего вызывает load()
. Если же пользователь ничего не
нажал, цикл прерывается по тайм-ауту и load()
загрузит файл
по умолчанию (/boot/loader). Функции ino_t lookup(char *filename)
и int
xfsread(ino_t inode, void *buf, size_t nbyte)
используются для считывания
содержимого файла в память. /boot/loader является двоичным
файлом формата ELF, однако его ELF заголовок дополнительно содержит структуру struct exec формата a.out. load()
просматривает ELF заголовок загрузчика, загружая содержимое /boot/loader в память и передаёт управление в точку входа
загрузчика:
sys/boot/i386/boot2/boot2.c: __exec((caddr_t)addr, RB_BOOTINFO | (opts & RBX_MASK), MAKEBOOTDEV(dev_maj[dsk.type], 0, dsk.slice, dsk.unit, dsk.part), 0, 0, 0, VTOP(&bootinfo));
Загрузчик (loader) также является клиентом BTX. Я не буду здесь его детально описывать, потому как есть исчерпывающая страница руководства loader(8), написанная Майком Смитом (Mike Smith). Нижележащие механизмы и BTX были описаны выше.
Основной задачей загрузчика является загрузка ядра. Когда ядро загружено в память, оно вызывается загрузчиком:
sys/boot/common/boot.c: /* Call the exec handler from the loader matching the kernel */ module_formats[km->m_loader]->l_exec(km);
Куда именно выполнение передаётся загрузчиком, т.е. что является самой точкой входа в ядро. Давайте взглянем на команду, которая компонует ядро:
sys/conf/Makefile.i386: ld -elf -Bdynamic -T /usr/src/sys/conf/ldscript.i386 -export-dynamic \ -dynamic-linker /red/herring -o kernel -X locore.o \ <lots of kernel .o files>
Несколько интересных вещей можно заметить в данной строке. Во-первых, ядро - это динамически скомпонованный двоичный файл формата ELF, однако динамическим компоновщиком для ядра является определённо фиктивный файл /red/herring. Во-вторых, просмотр файла sys/conf/ldscript.i386 даёт представление о том, с какими опциями используется ld при компиляции ядра. Одна из первых строк файла
sys/conf/ldscript.i386: ENTRY(btext)
говорит о том, что точка входа в ядро - символ `btext'. Данный символ определён в locore.s:
sys/i386/i386/locore.s: .text /********************************************************************** * * This is where the bootblocks start us, set the ball rolling... * */ NON_GPROF_ENTRY(btext)
Первое, что происходит - это установка регистра EFLAGS в предопределённое значение 0x00000002, затем инициализируются все сегментные регистры:
sys/i386/i386/locore.s /* Don't trust what the BIOS gives for eflags. */ pushl $PSL_KERNEL popfl /* * Don't trust what the BIOS gives for %fs and %gs. Trust the bootstrap * to set %cs, %ds, %es and %ss. */ mov %ds, %ax mov %ax, %fs mov %ax, %gs
btext вызывает процедуры recover_bootinfo()
, identify_cpu()
, create_pagetables()
, которые также определены в locore.s. Далее следует описание того, что они делают:
recover_bootinfo |
Эта процедура производит синтаксический разбор параметров, переданных ядру во время начальной загрузки. Ядро могло быть загружено тремя способами: загрузчиком, описанным выше, старыми загрузочными блоками диска и старой процедурой бездисковой загрузки. Эта функция определяет метод загрузки и сохраняет структуру struct bootinfo в памяти ядра. |
identify_cpu |
Эта функция пытается определить, на каком процессоре работает и сохраняет найденное
значение в переменной _cpu . |
create_pagetables |
Эта функция выделяет память под и заполняет Каталог Таблиц Страниц (Page Table Directory) в вершине области памяти ядра. |
Следующие шаги - включение VME, если процессор его поддерживает:
testl $CPUID_VME, R(_cpu_feature) jz 1f movl %cr4, %eax orl $CR4_VME, %eax movl %eax, %cr4
Затем включение страничного режима:
/* Now enable paging */ movl R(_IdlePTD), %eax movl %eax,%cr3 /* load ptd addr into mmu */ movl %cr0,%eax /* get control word */ orl $CR0_PE|CR0_PG,%eax /* enable paging */ movl %eax,%cr0 /* and let's page NOW! */
Следующие три строчки кода появляются из-за включения страничного режима, потому как для продолжения выполнения в виртуальном адресном пространстве необходим переход:
pushl $begin /* jump to high virtualized address */ ret /* now running relocated at KERNBASE where the system is linked to run */ begin:
Вызывается функция init386()
с указателем на первую
свободную физическую страницу, после чего происходит вызов mi_startup()
. init386
является
архитектурно-зависимой функцией инициализации, а mi_startup()
- архитектурно-независимой (префикс 'mi_' означает
машинно-независимый (Machine Independent)). Ядро никогда не возвращается из mi_startup()
, и вызвав её, оно заканчивает загрузку:
sys/i386/i386/locore.s: movl physfree, %esi pushl %esi /* value of first for init386(first) */ call _init386 /* wire 386 chip for unix operation */ call _mi_startup /* autoconfiguration, mountroot etc */ hlt /* never returns to here */
init386()
Функция init386()
определена в файле sys/i386/i386/machdep.c. Она выполняет низкоуровневую, специфичную
для i386 инициализацию. Переключение в защищённый режим было выполнено загрузчиком.
Загрузчик создал самую первую задачу, в котором ядро продолжает выполняться. Перед тем,
как обратиться к коду, я перечислю задачи, которые процессор должен завершить для
инициализации выполнения в защищённом режиме:
Инициализировать настраиваемые ядром параметры, переданные ему программой загрузки.
Подготовить GDT.
Подготовить IDT.
Инициализировать системную консоль.
Инициализировать DDB, если он включён в ядро.
Инициализировать TSS.
Подготовить LDT.
Установить pcb для proc0.
init386()
первым делом инициализирует настраиваемые
параметры, переданные из начальной загрузки. Это осуществляется путём установки указателя
на окружение (environment pointer, envp) и вызовом init_param1()
. Указатель envp был передан загрузчиком в структуре
bootinfo:
sys/i386/i386/machdep.c: kern_envp = (caddr_t)bootinfo.bi_envp + KERNBASE; /* Init basic tunables, hz etc */ init_param1();
Функция init_param1()
определена в файле sys/kern/subr_param.c. В этом файле определены некоторые переменные
sysctl и две функции: init_param1()
и init_param2()
, которые вызываются из init386()
:
sys/kern/subr_param.c hz = HZ; TUNABLE_INT_FETCH("kern.hz", &hz);
TUNABLE_<typename>_FETCH используется для получения значения из окружения:
/usr/src/sys/sys/kernel.h #define TUNABLE_INT_FETCH(path, var) getenv_int((path), (var))
Sysctl kern.hz - это тик системного таймера. Этот и другие
sysctl устанавливаются в init_param1()
: kern.maxswzone, kern.maxbcache, kern.maxtsiz, kern.dfldsiz, kern.dflssiz,
kern.maxssiz, kern.sgrowsiz.
Далее init386()
подготавливает глобальную таблицу
дескрипторов (GDT). Каждая задача, выполняющаяся на процессоре архитектуры x86, работает
в своём собственном виртуальном адресном пространстве, и это пространство адресуется
парой сегмент:смещение. Пусть, для примера, текущая инструкция, которая должна быть
выполнена процессором, располагается по адресу CS:EIP, тогда линейным виртуальным адресом
этой инструкции будет ''виртуальный адрес сегмента когда CS'' + EIP. Для удобства будем
считать, что сегменты начинаются с виртуального адреса 0 и заканчиваются на границе 4Гб.
Таким образом, линейный виртуальный адрес инструкции для этого примера просто будет равен
значению регистра EIP. Сегментные регистры, такие как CS, DS и другие являются
селекторами, т.е. индексами в GDT (если быть более точными, сам индекс не является
селектором, он является полем INDEX селектора). GDT в FreeBSD содержит дескрипторы 15
селекторов для каждого процессора:
sys/i386/i386/machdep.c: union descriptor gdt[NGDT * MAXCPU]; /* global descriptor table */ sys/i386/include/segments.h: /* * Entries in the Global Descriptor Table (GDT) */ #define GNULL_SEL 0 /* Null Descriptor */ #define GCODE_SEL 1 /* Kernel Code Descriptor */ #define GDATA_SEL 2 /* Kernel Data Descriptor */ #define GPRIV_SEL 3 /* SMP Per-Processor Private Data */ #define GPROC0_SEL 4 /* Task state process slot zero and up */ #define GLDT_SEL 5 /* LDT - eventually one per process */ #define GUSERLDT_SEL 6 /* User LDT */ #define GTGATE_SEL 7 /* Process task switch gate */ #define GBIOSLOWMEM_SEL 8 /* BIOS low memory access (must be entry 8) */ #define GPANIC_SEL 9 /* Task state to consider panic from */ #define GBIOSCODE32_SEL 10 /* BIOS interface (32bit Code) */ #define GBIOSCODE16_SEL 11 /* BIOS interface (16bit Code) */ #define GBIOSDATA_SEL 12 /* BIOS interface (Data) */ #define GBIOSUTIL_SEL 13 /* BIOS interface (Utility) */ #define GBIOSARGS_SEL 14 /* BIOS interface (Arguments) */
Заметьте, что эти #define сами не являются селекторами, а только полем INDEX селектора, так что они в действительности - индексы GDT. К примеру, действительный селектор для кода ядра (GCODE_SEL) имеет значение 0x08.
На следующем шаге инициализируется таблица дескрипторов прерываний (Interrupt Description Table, IDT). На эту таблицу ссылается процессор, когда возникают программные или аппаратные прерывания. Например, для осуществления системного вызова пользовательское приложение вызывает инструкцию INT 0x80. Это программное прерывание, поэтому процессор ищет запись с индексом 0x80 в IDT. Эта запись указывает на процедуру обработки прерывания, которая в нашем примере является шлюзом системных вызовов ядра. IDT может иметь максимум 256 (0x100) записей. Ядро располагает (allocates) NIDT записей для IDT, где NIDT имеет значение не более 256:
sys/i386/i386/machdep.c: static struct gate_descriptor idt0[NIDT]; struct gate_descriptor *idt = &idt0[0]; /* interrupt descriptor table */
Для каждого прерывания устанавливается соответствующий обработчик. Шлюз системных вызовов для INT 0x80 устанавливается так:
sys/i386/i386/machdep.c: setidt(0x80, &IDTVEC(int0x80_syscall), SDT_SYS386TGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));
Поэтому, когда пользовательское приложение выполняет инструкцию INT 0x80, управление переходит к функции _Xint0x80_syscall
, которая располагается в сегменте кода ядра и
будет выполнена c привилегиями диспетчера (supervisor):
Далее инициализируются консоль и DDB:
sys/i386/i386/machdep.c: cninit(); /* skipped */ #ifdef DDB kdb_init(); if (boothowto & RB_KDB) Debugger("Boot flags requested debugger"); #endif
Сегмент состояния задачи (Task State Segment, TSS) - ещё одна структура защищённого режима x86. TSS используется аппаратурой для загрузки сведений о задаче, когда происходит переключение задачи.
Локальная таблица дескрипторов (Local Descriptors Table, LDT) используется для обращения к коду и данным в пользовательском пространстве. Определенно несколько селекторов для указания на LDT. Это шлюзы системных вызовов и селекторы пользовательского кода и данных:
/usr/include/machine/segments.h #define LSYS5CALLS_SEL 0 /* forced by intel BCS */ #define LSYS5SIGR_SEL 1 #define L43BSDCALLS_SEL 2 /* notyet */ #define LUCODE_SEL 3 #define LSOL26CALLS_SEL 4 /* Solaris >= 2.6 system call gate */ #define LUDATA_SEL 5 /* separate stack, es,fs,gs sels ? */ /* #define LPOSIXCALLS_SEL 5*/ /* notyet */ #define LBSDICALLS_SEL 16 /* BSDI system call gate */ #define NLDT (LBSDICALLS_SEL + 1)
Далее происходит инициализация структуры Блока Управления Процессом (Process Control Block, PCB) (struct pcb) из proc0. proc0 это структура типа struct proc, которая описывает процесс ядра. Она всегда существует, пока работает ядро, потому объявлена как глобальная:
sys/kern/kern_init.c: struct proc proc0;
Структура struct pcb - составная часть proc структуры. Она (struct pcb) определена в /usr/include/machine/pcb.h и содержит информацию о процессе, специфичную для архитектуры i386, такую как значения регистров.
mi_startup()
Эта функция выполняет пузырьковую сортировку всех объектов инициализации системы, после чего вызывает эти объекты один за другим:
sys/kern/init_main.c: for (sipp = sysinit; *sipp; sipp++) { /* ... skipped ... */ /* Call function */ (*((*sipp)->func))((*sipp)->udata); /* ... skipped ... */ }
Несмотря на то, что подсистема sysinit описана в Руководстве для разработчиков, я всё же опишу здесь её внутренности.
Каждой объект инициализации системы (объект sysinit) создаётся посредством вызова макроса SYSINIT(). Давайте, для примера, рассмотрим sysinit объект announce. Этот объект выводит сообщение copyright:
sys/kern/init_main.c: static void print_caddr_t(void *data __unused) { printf("%s", (char *)data); } SYSINIT(announce, SI_SUB_COPYRIGHT, SI_ORDER_FIRST, print_caddr_t, copyright)
Идентификатором подсистемы для этого объекта является SI_SUB_COPYRIGHT (0x0800001), который следует сразу за SI_SUB_CONSOLE (0x0800000). Итак, copyright сообщение будет напечатано первым, сразу же после инициализации консоли.
Давайте посмотрим на то, что конкретно делает макрос SYSINIT(). Он расширяется (expands) в макрос C_SYSINIT(). Макрос C_SYSINIT() далее расширяется в определение статической структуры struct sysinit с вызовом другого макроса DATA_SET:
/usr/include/sys/kernel.h: #define C_SYSINIT(uniquifier, subsystem, order, func, ident) \ static struct sysinit uniquifier ## _sys_init = { \ subsystem, \ order, \ func, \ ident \ }; \ DATA_SET(sysinit_set,uniquifier ## _sys_init); #define SYSINIT(uniquifier, subsystem, order, func, ident) \ C_SYSINIT(uniquifier, subsystem, order, \ (sysinit_cfunc_t)(sysinit_nfunc_t)func, (void *)ident)
Макрос DATA_SET() расширяется в MAKE_SET(), и именно этот макрос является местом, где скрыта вся магия sysinit:
/usr/include/linker_set.h #define MAKE_SET(set, sym) \ static void const * const __set_##set##_sym_##sym = &sym; \ __asm(".section .set." #set ",\"aw\""); \ __asm(".long " #sym); \ __asm(".previous") #endif #define TEXT_SET(set, sym) MAKE_SET(set, sym) #define DATA_SET(set, sym) MAKE_SET(set, sym)
В нашем случае появится следующее определение:
static struct sysinit announce_sys_init = { SI_SUB_COPYRIGHT, SI_ORDER_FIRST, (sysinit_cfunc_t)(sysinit_nfunc_t) print_caddr_t, (void *) copyright }; static void const *const __set_sysinit_set_sym_announce_sys_init = &announce_sys_init; __asm(".section .set.sysinit_set" ",\"aw\""); __asm(".long " "announce_sys_init"); __asm(".previous");
Первая инструкция __asm создаст ELF секцию в пределах ядра. Это произойдёт во время компоновки ядра. Эта секция будет иметь имя .set.sysinit_set. Её содержимое - одно 32-битное значение - адрес структуры announce_sys_init, определяемое второй инструкцией __asm. Третья инструкция __asm помечает конец секции. Если директива с таким же именем секции встречалась раньше, то содержимое, т.е. 32-битное значение, будет добавлено к существующей секции, формируя таким образом массив 32-битных указателей.
Запустив objdump с двоичным файлом ядра в качестве параметра, вы можете заметить наличие таких маленьких секций:
% objdump -h /kernel 7 .set.cons_set 00000014 c03164c0 c03164c0 002154c0 2**2 CONTENTS, ALLOC, LOAD, DATA 8 .set.kbddriver_set 00000010 c03164d4 c03164d4 002154d4 2**2 CONTENTS, ALLOC, LOAD, DATA 9 .set.scrndr_set 00000024 c03164e4 c03164e4 002154e4 2**2 CONTENTS, ALLOC, LOAD, DATA 10 .set.scterm_set 0000000c c0316508 c0316508 00215508 2**2 CONTENTS, ALLOC, LOAD, DATA 11 .set.sysctl_set 0000097c c0316514 c0316514 00215514 2**2 CONTENTS, ALLOC, LOAD, DATA 12 .set.sysinit_set 00000664 c0316e90 c0316e90 00215e90 2**2 CONTENTS, ALLOC, LOAD, DATA
Этот снимок экрана демонстрирует, что секция .set.sysinit_set имеет размер 0x664 байт, поэтому 0x664/sizeof(void *) объектов sysinit скомпилировано в ядро. Другие секции, например .set.sysctl_set, представляют другие компоновочные наборы (linker sets).
При определении переменной типа struct linker_set содержимое секции .set.sysinit_set будет ''собрано'' в этой переменной:
sys/kern/init_main.c: extern struct linker_set sysinit_set; /* XXX */
Структура struct linker_set определена следующим образом:
/usr/include/linker_set.h: struct linker_set { int ls_length; void *ls_items[1]; /* really ls_length of them, trailing NULL */ };
Значение первого поля будет равно числу sysinit объектов. Вторым полем будет NULL-terminated массив указателей на эти объекты.
Вернёмся к обсуждению функции mi_startup()
. Теперь уже
должно быть ясно, как sysinit объекты организованы. Функция mi_startup()
сортирует их и каждого вызывает. Самый последний
объект - системный планировщик:
/usr/include/sys/kernel.h: enum sysinit_sub_id { SI_SUB_DUMMY = 0x0000000, /* not executed; for linker*/ SI_SUB_DONE = 0x0000001, /* processed*/ SI_SUB_CONSOLE = 0x0800000, /* console*/ SI_SUB_COPYRIGHT = 0x0800001, /* first use of console*/ ... SI_SUB_RUN_SCHEDULER = 0xfffffff /* scheduler: no return*/ };
Sysinit объект системного планировщика определён в файле sys/vm/vm_glue.c, и его точка входа - функция scheduler()
. Эта функция на самом деле является бесконечным
циклом и представляет процесс с PID 0 (процесс swapper). Структура proc0, упомянутая
раньше, используется для его описания.
Первый пользовательский процесс, имеющий имя init создаётся sysinit объектом init:
sys/kern/init_main.c: static void create_init(const void *udata __unused) { int error; int s; s = splhigh(); error = fork1(&proc0, RFFDG | RFPROC, &initproc); if (error) panic("cannot fork init: %d\n", error); initproc->p_flag |= P_INMEM | P_SYSTEM; cpu_set_fork_handler(initproc, start_init, NULL); remrunqueue(initproc); splx(s); } SYSINIT(init,SI_SUB_CREATE_INIT, SI_ORDER_FIRST, create_init, NULL)
Функция create_init()
выделяет новый процесс посредством
вызова fork1()
, но не помечает его, как работоспособного.
Когда этот новый процесс будет запланирован планировщиком на выполнение, будет вызвана
функция start_init()
. Она определена в файле init_main.c. Эта функция пытается загрузить и выполнить двоичный
файл init, пробуя сначала /sbin/init,
затем /sbin/oinit, /sbin/init.bak, и
наконец, /stand/sysinstall:
sys/kern/init_main.c: static char init_path[MAXPATHLEN] = #ifdef INIT_PATH __XSTRING(INIT_PATH); #else "/sbin/init:/sbin/oinit:/sbin/init.bak:/stand/sysinstall"; #endif
Эта глава поддерживается проектом FreeBSD SMP Next Generation Project. Комментарии и пожелания направляйте в Список рассылки, посвящённый поддержке многопроцессорности (SMP) во FreeBSD.
Этот документ описывает механизм блокировки, используемый в ядре FreeBSD для обеспечения эффективной поддержки нескольких процессоров в ядре. Блокировку можно рассматривать с нескольких точек зрения. Структуры данных могут быть защищены с помощью блокировок mutex или lockmgr(9). Несколько переменных защищены просто в силу атомарности используемых для доступа к ним операций.
Мьютекс (mutex) - это просто блокировка, используемая для реализации гарантированной исключительности. В частности, в каждый момент времени мьютексом может владеть только один объект. Если какой-то объект хочет получить мьютекс, который уже кто-то занял, он должен дождаться момента его освобождения. В ядре FreeBSD владельцами мьютексов являются процессы.
Мьютексы могут быть затребованы рекурсивно, но предполагается, что они занимаются на короткое время. В частности, владельцу мьютекса нельзя выдерживать паузу. Если вам нужно выполнить блокировку на время паузы, используйте блокировку через lockmgr(9).
Каждый мьютекс имеет несколько представляющих интерес характеристик:
Имя переменной struct mtx в исходных текстах ядра.
Имя мьютекса, назначенное ему через mtx_init
. Это имя
выводится в сообщениях трассировки KTR и диагностических предупреждающих и ошибочных
сообщениях и используется для идентификации мьютексов в отладочном коде.
Тип мьютекса в терминах флагов MTX_*
. Значение каждого
флага связано с его смыслом так, как это описано в mutex(9).
MTX_DEF
Sleep-мьютекс
MTX_SPIN
Spin-мьютекс
MTX_RECURSE
Этому мьютексу разрешается блокировать рекурсивно.
Список структур данных или членов структур данных, которые защищает этот мьютекс. Для
членов структур данных имя будет в форме имя
структуры
.имя члена структуры
.
Функции, которые можно вызвать, если этот мьютекс занят.
Таблица 2-1. Список мьютексов
Имя переменной | Логическое имя | Тип | Защиты | Зависимые функции |
---|---|---|---|---|
sched_lock | ''sched lock'' | MTX_SPIN | MTX_RECURSE
|
_gmonparam , cnt.v_swtch ,
cp_time , curpriority , mtx .mtx_blocked , mtx .mtx_contested , proc .p_procq , proc .p_slpq , proc .p_sflag , proc .p_stat , proc .p_estcpu , proc .p_cpticks proc .p_pctcpu , proc .p_wchan , proc .p_wmesg , proc .p_swtime , proc .p_slptime , proc .p_runtime , proc .p_uu , proc .p_su , proc .p_iu , proc .p_uticks , proc .p_sticks , proc .p_iticks , proc .p_oncpu , proc .p_lastcpu , proc .p_rqindex , proc .p_heldmtx , proc .p_blocked , proc .p_mtxname , proc .p_contested , proc .p_priority , proc .p_usrpri , proc .p_nativepri , proc .p_nice , proc .p_rtprio , pscnt , slpque , itqueuebits , itqueues , rtqueuebits , rtqueues , queuebits , queues , idqueuebits , idqueues , switchticks , |
setrunqueue , remrunqueue ,
mi_switch , chooseproc , schedclock , resetpriority , updatepri , maybe_resched , cpu_switch , cpu_throw , need_resched , resched_wanted , clear_resched , aston , astoff , astpending , calcru , proc_compare |
vm86pcb_lock | ''vm86pcb lock'' | MTX_DEF |
vm86pcb |
vm86_bioscall |
Giant | ''Giant'' | MTX_DEF | MTX_RECURSE
|
nearly everything | lots |
callout_lock | ''callout lock'' | MTX_SPIN | MTX_RECURSE
|
callfree , callwheel , nextsoftcheck , proc .p_itcallout , proc .p_slpcallout , softticks , ticks |
Эти блокировки обеспечивают базовый тип функциональности - на чтение/запись и могут поддерживаться процессами, находящимся в состоянии ожидания. На текущий момент они реализованы в lockmgr(9).
Переменной, защищенной атомарно, является особая переменная, которая не защищается явной блокировкой. Вместо этого для доступа к данным переменных используются специальные атомарные операции, как описано в atomic(9). Лишь несколько переменных используются таким образом, хотя другие примитивы синхронизации, такие как мьютексы, реализованы с атомарно защищенными переменными.
mtx
.mtx_lock
Объекты ядра (Kernel Objects), или сокращенно Kobj, дают систему программирования на C для ядра, ориентированную на объекты. Таким образом, обрабатываемые данные включают описание того, как их обрабатывать. Это позволяет добавлять и удалять функции из интерфейса во время работы и без нарушения двоичной совместимости.
Набор данных - структура данных - распределение данных.
Операция - функция.
Один или более методов.
Стандартный набор из одного или более методов.
Kobj работает, создавая описания методов. Каждое описание содержит уникальный идентификатор, а также функцию по умолчанию. Адрес описания используется для однозначной идентификации метода внутри таблицы методов класса.
Класс строится созданием таблицы методов, связывающей одну или большее количество функций с описаниями методов. Перед использованием класс компилируется. При компиляции выделяется кэш и он связывается с классом. Каждому описанию метода в таблице методов класса ставится в соответствие уникальный идентификатор, если это уже не сделано при компиляции другого связанного класса. Для каждого используемого метода скриптом генерируется функция для проверки аргументов и автоматической отсылки к описанию метода для поиска. Генерируемая функция ищет метод при помощи уникального идентификатора, связанного с описание метода, используемого в качестве хэша, в кэше, связанном с классом объекта. Если метод не кэширован, то генерируемая функция будет использовать таблицу класса для поиска метода. Если метод найден, то используется соответствующая функция из класса; в противном случае используется функция по умолчанию, связанная с описанием метода.
Это описание можно изобразить следующим образом:
object->cache<->class
struct kobj_method
void kobj_class_compile(kobj_class_t cls); void kobj_class_compile_static(kobj_class_t cls, kobj_ops_t ops); void kobj_class_free(kobj_class_t cls); kobj_t kobj_create(kobj_class_t cls, struct malloc_type *mtype, int mflags); void kobj_init(kobj_t obj, kobj_class_t cls); void kobj_delete(kobj_t obj, struct malloc_type *mtype);
KOBJ_CLASS_FIELDS KOBJ_FIELDS DEFINE_CLASS(name, methods, size) KOBJMETHOD(NAME, FUNC)
Первый шаг при использовании Kobj заключается в создании интерфейса. Создание интерфейса включает создание шаблона, который может использоваться скриптом src/sys/kern/makeobjops.pl для генерации файла объявлений и кода для объявлений метода и функций поиска методов.
Внутри этого шаблона используются следующие ключевые слова: #include, INTERFACE, CODE, METHOD, STATICMETHOD и DEFAULT.
Директива #include и то, что следует далее, без изменений копируется в начало файла генерируемого кода.
Например:
#include <sys/foo.h>
Ключевое слово INTERFACE используется для определения имени интерфейса. Это имя объединяется с именем каждого метода в виде [имя интерфейса]_[имя метода]. Он имеет синтаксис INTERFACE [имя интерфейса];.
Например:
INTERFACE foo;
Ключевое слово CODE копирует свои аргументы без изменений в файл кода. Он имеет синтаксис CODE { [нечто] };
Например:
CODE { struct foo * foo_alloc_null(struct bar *) { return NULL; } };
Ключевое слово METHOD описывает метод. Его синтаксис METHOD [возвращаемый тип] [имя метода] { [объект [, аргументы]] };
For example:
METHOD int bar { struct object *; struct foo *; struct bar; };
Ключевое слово DEFAULT может следовать за ключевым словом METHOD. Оно расширяет ключевое слово METHOD добавлением к методу функции, используемой по умолчанию. Расширенный синтаксис имеет вид METHOD [возвращаемый тип] [имя метода] { [объект; [другие аргументы]] }DEFAULT [функция по умолчанию];
Например:
METHOD int bar { struct object *; struct foo *; int bar; } DEFAULT foo_hack;
Ключевое слово STATICMETHOD используется так же, как и METHOD, за исключением того, что данные kobj не располагаются в начале структуры объекта, поэтому приведение к типу kobj_t будет некорректным. Вместо этого STATICMETHOD опирается на данные Kobj, на которые ссылаются как 'ops'. Это также полезно для непосредственного вызова методов из таблицы методов класса.
Другие законченные примеры:
src/sys/kern/bus_if.m src/sys/kern/device_if.m
Второй шаг при использовании Kobj заключается в создании класса. Класс состоит из
имени, таблицы методов и размера объектов, если использовались механизмы обработки
объекта Kobj. Для создания класса воспользуйтесь макросом DEFINE_CLASS()
. Для создания таблицы методов создайте массив из
kobj_method_t, завершающийся NULL. Каждый элемент, не равный NULL, может быть создан при
помощи макроса KOBJMETHOD()
.
Например:
DEFINE_CLASS(fooclass, foomethods, sizeof(struct foodata)); kobj_method_t foomethods[] = { KOBJMETHOD(bar_doo, foo_doo), KOBJMETHOD(bar_foo, foo_foo), { NULL, NULL} };
Класс должен быть ''откомпилирован''. В зависимости от состояния системы в момент,
когда класс инициализирует статически выделяемый кэш, используется ''таблица ops''. Это
может быть достигнуто объявлением struct kobj_ops
и
использованием kobj_class_compile_static();
, в противном
случае должна быть использована функция kobj_class_compile()
.
Третий шаг в использовании Kobj включает и то, как определить объект. Процедуры
создания объекта Kobj предполагают, что данные Kobj располагаются в начале объекта. Если
это не подходит, то вам нужно распределить объект самостоятельно и затем воспользоваться
функцией kobj_init()
над его частью, относящейся к Kobj; в
противном случае вы можете использовать функцию kobj_create()
для выделения и инициализации части Kobk объекта
автоматически. kobj_init()
можно также использовать для
изменения класса, который использует объект.
Для интеграции Kobj в объект, вы должны использовать макрос KOBJ_FIELDS.
Например
struct foo_data { KOBJ_FIELDS; foo_foo; foo_bar; };
Последний шаг в использовании Kobj сводится просто к использованию сгенерированных функция для вызова требуемого метода из класса объекта. Это также просто, как использование имени интерфейса и имени метода с некоторыми модификациями. Имя интерфейса должно быть объединено с именем метода через символ '_' между ними, все в верхнем регистре.
К примеру, если имя интерфейса было foo, а метод назывался bar, то вызов будет таким:
[return value = ] FOO_BAR(object [, other parameters]);
Когда объект, распределенный через kobj_create()
, больше
не нужен, то над ним может быть выполнена функция kobj_delete()
, а когда класс больше не используется, то над ним
может быть выполнен вызов kobj_class_free()
.
Перевод на русский язык: Сергей Любка (<devnull@asitatech.ie>
)
На большинстве UNIX систем, пользователь root имеет неограниченные права. Это потенциально небезопасно. Если атакующий сумеет получить права root, любая функция системы будет под его контролем. FreeBSD имеет ряд параметров ядра, ограничивающих безраздельные права root для уменьшения возможного ущерба от атакующего. Например, одним из таких параметров является уровень защиты (secure level). Начиная с версии FreeBSD 4.0, другим таким параметром является jail(8). Jail (jail в переводе - тюрьма, заключение) делает chroot окружения, и накладывает определенные ограничения на процессы, которые в нем порождены. Например, jailed процесс (т.е. процесс, сделавший вызов jail) не может влиять на процессы вне jail, делать некоторые системные вызовы, или каким-либо образом повреждать другие части ОС.
Jail становится новой моделью защиты. Администраторы запускают потенциально уязвимые сервисы, такие как Apache, BIBD и sendmail, внутри jail; и если атакующий даже и получит права root внутри Jail, то это не приведет к краху всей системы. Эта статья концентрируется на внутренней реализации Jail, а также предложит некоторые улучшения к теперешней реализации. Если Вас интересует административная сторона установки Jail, я рекомендую просмотреть мою статью в Sys Admin Magazine за май 2001, под заглавием "Securing FreeBSD using Jail".
Jail состоит из двух частей: непривилегированной (user-space) программы, jail, и кода в ядре: системного вызова jail и соответствующих ограничений. Сначала я рассмотрю user-space утилиту jail, а затем то, как jail реализованa в ядре.
Исходный код утилиты jail находится в каталоге /usr/src/usr.sbin/jail, в файле jail.c. Утилита принимает следующие аргументы командной строки: путь к jail, имя хоста, ip адрес, и команду.
В файле jail.c первой вещью, которую я хотел бы отметить, это декларация важной структуры, struct jail j, которая включена из файла /usr/include/sys/jail.h.
Вот определение это структуры:
/usr/include/sys/jail.h: struct jail { u_int32_t version; char *path; char *hostname; u_int32_t ip_number; };
Как можно заметить, в ней есть поле для каждого из аргументов, передаваемого утилите jail, и они выставляются при запуске jail.
/usr/src/usr.sbin/jail.c j.version = 0; j.path = argv[1]; j.hostname = argv[2];
Одним из аргументов, передаваемых jail, есть ip адрес, по которому jail может быть доступен из сети. Jail переводит переданный ip адрес в big endian формат (network byte order), и сохраняет его в j (структуре jail).
/usr/src/usr.sbin/jail/jail.c: struct in.addr in; ... i = inet.aton(argv[3], &in); ... j.ip_number = ntohl(in.s.addr);
Функция inet_aton(3)
интерпретирует переданную ей строку как интернет адрес, и сохраняет его по переданному
указателю на структуру in_addr. Функция ntohl()
изменяет
порядок следования байтов на сетевой (big endian), и получившееся значение сохраняется в
поле ip адреса структуры jail.
Наконец, утилита jail выполняет системный вызов jail, чем ``заключает в тюрьму'' сама себя, и порождает дочерний процесс, который затем выполняет команду, указанную в командной строке jail, используя вызов execv(3):
/usr/src/sys/usr.sbin/jail/jail.c i = jail(&j); ... i = execv(argv[4], argv + 4);
Системному вызову jail, как видно из листинга, передается указатель на заполненную структуру jail. Теперь я собираюсь обсудить, как Jail реализован в ядре.
Заглянем в файл /usr/src/sys/kern/kern_jail.c. Это файл, в котором определены системный вызов jail, соответствующие параметры ядра (sysctls), и сетевые функции.
В файле kern_jail.c определены следующие параметры ядра:
/usr/src/sys/kern/kern_jail.c:
int jail_set_hostname_allowed = 1;
SYSCTL_INT(_jail, OID_AUTO, set_hostname_allowed, CTLFLAG_RW,
&jail_set_hostname_allowed, 0,
"Processes in jail can set their hostnames");
int jail_socket_unixiproute_only = 1;
SYSCTL_INT(_jail, OID_AUTO, socket_unixiproute_only, CTLFLAG_RW,
&jail_socket_unixiproute_only, 0,
"Processes in jail are limited to creating UNIX®/IPv4/route sockets only
");
int jail_sysvipc_allowed = 0;
SYSCTL_INT(_jail, OID_AUTO, sysvipc_allowed, CTLFLAG_RW,
&jail_sysvipc_allowed, 0,
"Processes in jail can use System V IPC primitives");
Каждый из этих параметров доступен пользователю через утилиту sysctl. В ядре эти параметры распознаются по уникальному имени; например, имя первого параметра будет jail.set.hostname.allowed.
Как и все другие системные вызовы, вызов jail(2) принимает два аргумента, struct proc *p и struct jail_args *uap. p - это указатель на структуру proc текущего процесса, а uap - это аргумент, переданный утилитой jail системному вызову jail(2):
/usr/src/sys/kern/kern_jail.c: int jail(p, uap) struct proc *p; struct jail_args /* { syscallarg(struct jail *) jail; } */ *uap;
Таким образом, uap->jail будет указывать на структуру
jail, переданную системному вызову. Далее, структура jail копируется в адресное
пространство ядра с помощью функции copyin()
, которая
принимает три аргумента: данные, которые необходимо скопировать (uap->jail), где их сохранить (j) и
размер копируемых данных. Структура jail, переданная как параметр системному вызову,
копируется в пространство ядра и сохраняется в другой структуре jail, j.
/usr/src/sys/kern/kern_jail.c: error = copyin(uap->jail, &j, sizeof j);
В файле jail.h определена другая важная структура - структура prison (pr). Эта структура используется исключительно ядром. Системный вызов jail(2) копирует все из структуры jail в структуру prison. Вот определение структуры prison:
/usr/include/sys/jail.h: struct prison { int pr_ref; char pr_host[MAXHOSTNAMELEN]; u_int32_t pr_ip; void *pr_linux; };
Далее, системный вызов jail() выделяет память для структуры prison и копирует данные:
/usr/src/sys/kern/kern_jail.c: MALLOC(pr, struct prison *, sizeof *pr , M_PRISON, M_WAITOK); bzero((caddr_t)pr, sizeof *pr); error = copyinstr(j.hostname, &pr->pr_host, sizeof pr->pr_host, 0); if (error) goto bail;
Наконец, jail() делает вызов chroot(2) для указанного пути. Вызову chroot() передаются два аргумента: первый - это p, который представляет вызвавший процесс, а второй - указатель на структуру chroot args. Структура chroot args содержит путь, который станет новым корнем. Путь, указанный в структуре jail, копируется в структуру chroot args и используется далее:
/usr/src/sys/kern/kern_jail.c: ca.path = j.path; error = chroot(p, &ca);
Следующие три строки кода очень важны, поскольку указывают, как ядро распознает jailed процесс. Каждый процесс в UNIX описывается своей структурой proc. Определение структуры proc можно посмотреть в /usr/include/sys/proc.h. К примеру, аргумент p, передаваемый в каждый системный вызов есть на самом деле указатель на эту структуру, как было указано выше. Структура proc содержит поля, описывающие владельца процесса (p_cred), лимиты ресурсов (p_limit), и так далее. В определении структуры есть также указатель на структуру prison (p_prison):
/usr/include/sys/proc.h: struct proc { ... struct prison *p_prison; ... };
В файле kern_jail.c, вызов jail() копирует структуру pr, заполненную в соответствии с информацией из структуры jail, в структуру p->p_prison. Затем делается побитовое OR p->p_flag и константы P_JAILED, что в дальнейшем даст возможность распознать этот процесс как jailed. Родительский процесс для любого дочернего процесса внутри jail есть программа jail. Когда дочерний процесс делает execve(), он наследует поля структуры proc от родительского, и, таким образом, также имеет в поле p->p_flag выставленный флаг P_JAILED, и заполненную структуру p->p_prison.
/usr/src/sys/kern/kern_jail.c p->p.prison = pr; p->p.flag |= P.JAILED;
Когда процесс порождает дочерний процесс, системный вызов fork(2) работает несколько иначе для процесса в jail. fork(2) принимает два аргумента - указателя на структуру proc, p1 и p2. Первый указатель, p1, указывает на структуру proc родительского процесса, а второй, p2 - дочернего. Структура, на которую указывает p2, не заполнена. После копирования всех необходимых данных между структурами, fork(2) проверяет, не заполнена ли структура p->p_prison у дочернего процесса, и если заполнена, то увеличивает счетчик pr.ref на единицу и выставляет флаг P_JAILED в поле p_flag:
/usr/src/sys/kern/kern_fork.c: if (p2->p_prison) { p2->p_prison->pr_ref++; p2->p_flag |= P_JAILED; }
Для реализации jail, во многих местах в коде ядра встречаются соответствующие проверки и ограничения. Как правило, большинство этих проверок сводятся к проверке, является ли данный процесс ``заключенным'' (jailed) и возврат кода ошибки, если да. К примеру:
if (p->p_prison) return EPERM;
Подсистема межпроцессного взаимодействия SysV основана на сообщениях. Процессы могут посылать сообщения друг другу с различной информацией. Для отсылки сообщений используются следующие функции: msgsys, msgctl, msgget, msgsend и msgrcv. Ранее я упоминал о том, что существует ряд параметров ядра, установка или сброс которых могут повлиять на поведение Jail. Одним из таких параметров является jail_sysvipc_allowed. На большинстве систем этот параметр установлен в 0. Если его установить в 1, это может свести на нет само использование Jail, так как привилегированный пользователь внутри jail сможет посылать сообщения процессам вне jail, и таким образом, влиять на них. Отличие между сигналом и сообщением SysV состоит в том, что сигнал можно рассматривать как сообщение с одним полем данных, а именно, номером сигнала, когда как сообщения SysV могут содержать большее количество полей данных:
/usr/src/sys/kern/sysv_msg.c:
msgget(3): msgget возвращает (и, возможно, создает) дескриптор очереди сообщений для дальнейшей отсылки/приема сообщений. Очередь сообщений, где хранятся сообщения, посланные процессами, располагается в пространстве ядра.
msgctl(3): Используя эту функцию, процесс может запросить текущий статус очереди сообщений по ее дескриптору.
msgsnd(3): msgsnd отсылает сообщение в очередь сообщений.
msgrcv(3): Процесс вызывает эту функцию для извлечения сообщения из очереди сообщений
Для каждого из этих системных вызовов, в коде присутствует следующая проверка:
/usr/src/sys/kern/sysv msg.c: if (!jail.sysvipc.allowed && p->p_prison != NULL) return (ENOSYS);
Системные вызовы, связанные с семафорами, позволяют различным процессам синхронизировать выполнения путем выполнения атомарных операций над специальными объектами - семафорами. Семафоры - один из способов блокировки ресурсов. Процесс, ждущий освобождения семафора, будет находиться в состоянии сна до тех пор, пока семафор не будет освобожден. Для процесса в jail недоступны следующие вызовы: semsys, semget, semctl и semop.
/usr/src/sys/kern/sysv_sem.c:
semctl(2)(id, num, cmd, arg): Semctl выполняет операцию cmd над семафором, указанном id.
semget(2)(key, nsems, flag): Semget создает массив семафоров для ключа key.
Key и flag имеют то же значение, что и для вызова msgget.
semop(2)(id, ops, num): Semop производит атомарные операции, указанные op, над семафорами, указанными id.
SysV IPC позволяет процессам совместно использовать память. Процессы могут совместно использовать части своих адресных пространств и таким образом непосредственно обращаться к памяти другого процесса. Для процесса в jail недоступны следующие вызовы: shmdt, shmat, oshmctl, shmctl, shmget, и shmsys.
shmctl(2)(id, cmd, buf): shmctl производит различные управляющие операции над блоком памяти, указанном id.
shmget(2)(key, size, flag): shmget возвращает или создает новый блок памяти размером в size байт.
shmat(2)(id, addr, flag): shmat присоединяет блок памяти, указанным id к адресному пространству процесса.
shmdt(2)(addr): shmdt detaches отсоединяет блок памяти, ранее присоединенный по адресу addr.
Внутри jail системный вызов socket(2) и связанные с ним низкоуровневые функции работают особым образом. Для того чтобы определить, разрешено ли создание того или иного сокета, socket(2) сначала проверяет, установлен ли параметр ядра jail.socket.unixiproute.only. Если установлен, то сокет можно создать только для доменов PF_LOCAL, PF_INET или PF_ROUTE. Иначе, возвращается ошибка:
/usr/src/sys/kern/uipc_socket.c: int socreate(dom, aso, type, proto, p) ... register struct protosw *prp; ... { if (p->p_prison && jail_socket_unixiproute_only && prp->pr_domain->dom_family != PR_LOCAL && prp->pr_domain->dom_family != PF_INET && prp->pr_domain->dom_family != PF_ROUTE) return (EPROTONOSUPPORT); ... }
Berkeley Packet Filter обеспечивает интерфейс к уровню канала данных. Функция bpfopen() открывает устройство Ethernet. Процессам внутри jail вызов этой функции запрещен:
/usr/src/sys/net/bpf.c: static int bpfopen(dev, flags, fmt, p) ... { if (p->p_prison) return (EPERM); ... }
Существует ряд распространенных сетевых протоколов, таких как TCP, UDP, IP, ICMP. IP и ICMP находятся на одном уроне стека: на сетевом. Для предотвращения привязки jailed процесса к определенному порту, предусмотрен ряд мер: процессу позволено сделать bind(), только если установлен параметр nam. nam - это указатель на структуру sockaddr, которая указывает на адрес, к которому привязать сервис. В функции pcbbind, sin - это указатель на структуру sockaddr.in, содержащую порт, адрес и домен сокета, для которого требуется привязка.
/usr/src/sys/kern/netinet/in_pcb.c: int in.pcbbind(int, nam, p) ... struct sockaddr *nam; struct proc *p; { ... struct sockaddr.in *sin; ... if (nam) { sin = (struct sockaddr.in *)nam; ... if (sin->sin_addr.s_addr != INADDR_ANY) if (prison.ip(p, 0, &sin->sin.addr.s_addr)) return (EINVAL); .... } ... }
Вы можете спросить, что делает функция prison_ip(). prison.ip принимает три аргумента: текущий процесс (указываемый p), флаги, и ip адрес. Она возвращает 1 если ip адрес принадлежит jail и 0, если нет. Как можно видеть из фрагмента кода, если ip адрес принадлежит jail, сокету не будет позволено привязаться к порту.
/usr/src/sys/kern/kern_jail.c: int prison_ip(struct proc *p, int flag, u_int32_t *ip) { u_int32_t tmp; if (!p->p_prison) return (0); if (flag) tmp = *ip; else tmp = ntohl (*ip); if (tmp == INADDR_ANY) { if (flag) *ip = p->p_prison->pr_ip; else *ip = htonl(p->p_prison->pr_ip); return (0); } if (p->p_prison->pr_ip != tmp) return (1); return (0); }
Процессам в jail не позволено привязывать сервисы к ip адресам, не принадлежащим jail. Это ограничение также есть в функции in_pcbbind:
/usr/src/sys/net inet/in_pcb.c if (nam) { ... lport = sin->sin.port; ... if (lport) { ... if (p && p->p_prison) prison = 1; if (prison && prison_ip(p, 0, &sin->sin_addr.s_addr)) return (EADDRNOTAVAIL);
Даже пользователь root внутри jail не может устанавливать специальные флаги для файла, такие как immutable, append, no unlink, если securelevel больше 0:
/usr/src/sys/ufs/ufs/ufs_vnops.c: int ufs.setattr(ap) ... { if ((cred->cr.uid == 0) && (p->prison == NULL)) { if ((ip->i_flags & (SF_NOUNLINK | SF_IMMUTABLE | SF_APPEND)) && securelevel > 0) return (EPERM); }
SYSINIT является общим механизмом для сортировки и диспетчеризации вызовов. В настоящее время FreeBSD использует его для динамической инициализации ядра. SYSINIT позволяет переорганизовывать подсистемы ядра FreeBSD, а также добавлять, удалять и замещать их на этапе компоновки ядра, во время его загрузки или загрузки одного из его модулей, без необходимости редактировать статически организованный порядок инициализации и перекомпилировать ядро. Эта система позволяет также модулям ядра, которые сейчас называются KLD, компилироваться отдельно, компоноваться и инициализироваться во время загрузки, и к тому же загружаться позже, при уже работающей системе. Это достигается при помощи ''компоновщика ядра'' и ''компоновочных наборов''.
Техника компоновки, при которой компоновщик переносит статически объявленные данные посредством исходных файлов программы в один сплошной адресуемый блок данных.
SYSINIT основан на возможности компоновщика брать статические данные, объявленные во многих местах, из исходного кода программы и группировать их вместе как один сплошной блок данных. Эта техника компоновки называется ''linker set'' (компоновочный набор). SYSINIT использует два набора компоновки для работы с двумя наборами данных, содержащих все последовательности вызовов, функцию и указатель на данные для передачи этой функции.
SYSINIT использует два приоритета при организации последовательности вызовов функций. Первый приоритет это идентификатор (ID) подсистемы, дающий общий порядок диспетчеризации функции через SYSINIT. Текущие предопределенные ID перечислены в файле <sys/kernel.h> в списке sysinit_sub_id. Второй используемый приоритет является порядковым номером элемента в подсистеме. Текущие предопределенные порядковые номера элементов подсистемы находятся в <sys/kernel.h> с списке sysinit_elem_order.
На данный момент есть два применения SYSINIT. Диспетчеризация функций при загрузке системы и модулей ядра, и диспетчеризация функций при выключении системы и выгрузке модулей ядра. Подсистемы ядра часто используют SYSINIT при загрузке системы для инициализации структур данных. К примеру, подсистема планирования процессов использует SYSINIT для инициализации структуры с данными живой очереди. Драйверы устройств не должны напрямую использовать SYSINIT(). Вместо этого, драйверы реальных устройств должны использовать DRIVER_MODULE() для вызова функции, которая определит устройство, и если оно присутствует - произведёт его инициализацию. Также будут сделаны некоторые вещи, специфичные для устройств и только после этого будет произведён вызов SYSINIT(). Для псевдо устройств используйте DEV_MODULE().
<sys/kernel.h>
SYSINIT(uniquifier, subsystem, order, func, ident) SYSUNINIT(uniquifier, subsystem, order, func, ident)
Макрос SYSINIT() создает необходимые данные SYSINIT в начальном наборе данных SYSINIT для того, чтобы SYSINIT сортировал и диспетчеризировал функцию при запуске системы и загрузке модуля. SYSINIT() принимает уникальный идентификатор, который используется в SYSINIT для идентификации данных диспетчеризации конкретной функции, порядковый номер подсистемы, порядковый номер элемента подсистемы, вызываемую функцию и данные для передачи в функцию. Все функции должны принимать в качестве параметра статический указатель.
Пример 5-1. Пример SYSINIT()
#include <sys/kernel.h> void foo_null(void *unused) { foo_doo(); } SYSINIT(foo, SI_SUB_FOO, SI_ORDER_FOO, foo_null, NULL); struct foo foo_voodoo = { FOO_VOODOO; } void foo_arg(void *vdata) { struct foo *foo = (struct foo *)vdata; foo_data(foo); } SYSINIT(bar, SI_SUB_FOO, SI_ORDER_FOO, foo_arg, &foo_voodoo);
Заметьте, что SI_SUB_FOO и SI_ORDER_FOO должны присутствовать в перечислениях sysinit_sub_id и sysinit_elem_order, как упомянуто выше. Либо используйте существующие, либо добавляйте свои в перечисления. Вы также можете использовать математику для более тонкого настраивания порядка, который будет проходить SYSINIT. Ниже показан пример SYSINIT, который должен быть запущен перед действиями SYSINIT, настраивающими параметры ядра.
Макрос SYSUNINIT() ведет себя похоже на макрос SYSINIT(), за исключением того, что он добавляет данные SYSINIT к набору данных закрытия SYSINIT.
Пример 5-3. Пример SYSUNINIT()
#include <sys/kernel.h> void foo_cleanup(void *unused) { foo_kill(); } SYSUNINIT(foobar, SI_SUB_FOO, SI_ORDER_FOO, foo_cleanup, NULL); struct foo_stack foo_stack = { FOO_STACK_VOODOO; } void foo_flush(void *vdata) { } SYSUNINIT(barfoo, SI_SUB_FOO, SI_ORDER_FOO, foo_flush, &foo_stack);
Перевод на русский язык: Андрей Захватов (<andy@FreeBSD.org>
)
Физическая память управляется на уровне отдельных страниц посредством структур vm_page_t. Страницы физической памяти разделены на категории через помещение соответствующих им структур vm_page_t в одну из нескольких очередей страниц.
Страница может находиться в связанном, активном, неактивном, кэшированном или свободном состоянии. Кроме случая связанного состояния, страница обычно помещается в двойной связный список очереди, соответствующей состоянию страницы. Закрепленные страницы не помещаются ни в какую очередь.
Для реализации алгоритма подгонки страниц во FreeBSD в основном применяется очередь для кэшированных и свободных страниц. Каждое из этих состояний затрагивает несколько очередей, которые строятся согласно размерам кэшей L1 и L2 процессора. Когда требуется выделение новой страницы, FreeBSD пытается получить ту, что расположена в достаточной мере рядом с точки зрения кэшей L1 и L2 относительно объекта VM, для которого выделяется страница.
Кроме того, страница может удерживаться счетчиком ссылок или может быть заблокирована счетчиком занятости. Система VM также реализует состояние ''безусловной блокировки'' для страницы установкой бита PG_BUSY в поле флагов страницы.
Говоря общими словами, каждая из очередей страниц работает по принципу LRU. Первоначально страница обычно приводится в связанное или активное состояние. Если она связана, то страница обычно указана где-то в таблице страниц. Система VM отслеживает устаревание страницы, сканируя страницы в более активной очереди страниц (LRU) для того, чтобы переместить их в менее активную очередь страниц. Страницы, которые перемещены в кэш, остаются связанными с объектом VM, но являются кандидатами на немедленное повторное использование. Страницы в очереди свободных страниц действительно являются свободными. FreeBSD пытается минимизировать количество страниц в очереди свободных страниц, однако для обеспечения выделения страниц во время обработки прерываний должно поддерживаться некоторое минимальное количество действительно свободных страниц.
Если процесс пытается обратиться к странице, которой не существует в его таблице страниц, но она имеется в одной из очередей страниц (например, в очереди неактивных страниц или очереди кэша), возникает сравнительно легко обрабатываемая ошибка отсутствия страницы, что приводит к повторной активации страницы. Если страницы вообще нет в системной памяти, то процесс должен быть блокирован, пока страница не будет взята с диска.
FreeBSD динамически изменяет свои очереди страниц, и пытается поддерживать разумное соотношение страниц в различных очередях, а также пытается поддерживать разумную схему работы с чистыми и грязными страницами. Количество случающихся перемещений зависит от нагрузки на память системы. Это перемещение выполняется даемоном выгрузки страниц и затрагивает стирание грязных страниц (синхронизируя их с хранилищем), пометку страниц при активной работе с ними (обновляя их расположение в очередях LRU или перемещая их между очередями), постепенное перемещение страниц между очередями при нарушении баланса между очередями, и так далее. VM-система FreeBSD старается обработать достаточное количество ошибок доступа к странице для реактивации, чтобы определить реальную активность или простой страницы. Это приводит к повышению качества решений, принимаемых при стирании или выгрузке страницы в раздел подкачки.
Во FreeBSD реализована идея ''объекта VM'' общего вида. Объекты VM могут быть связаны с хранилищами различного типа--без долговременного хранения, с хранением в области подкачки, с хранением на физическом устройстве или с хранением в файле. Так как файловая система использует одни и те же объекты VM для управления внутренними данными, связанными с файлами, то в результате получается универсальный буферизирующий кэш.
Объекты VM могут затеняться. Это значит, что они могут выстраиваться один над другим. Например, вы можете иметь объект VM с хранением в области подкачки, который выстроен над VM-объектом с хранением в файле, для реализации операции mmap() типа MAP_PRIVATE. Такое построение также используется для реализации различных функций при совместном использовании, включая копирование при записи для разветвляющегося адресного пространства.
Должно быть отмечено, что vm_page_t может быть одновременно связана только с одним объектов VM. Затенение VM-объекта реализует эффективное совместное использование одной и той же страницы между несколькими экземплярами.
Объектам VM, хранящимся в vnode-узлах, таким, как объекты с хранением в файле, как правило, нужно поддерживать собственную информацию о чистоте/использованности независимо от предположении системы VM о чистоте/использованности. Например, когда система VM решает синхронизировать физическую страницу с ее хранилищем, то ей нужно помечать страницу как очищенную перед тем, как она действительно будет записана в хранилище. Кроме того, файловым системам нужно отображать части файла или метаданных файла в KVM для работы с ним.
Структуры, используемые для этого, известны как буферы файловой системы, struct buf, или bp. Когда файловой системе нужно произвести операцию с частью объекта VM, она обычно отображает часть объекта в struct buf и отображает страницы из struct buf в KVM. Таким же образом дисковый ввод/вывод обычно осуществляется отображением частей объектов в буферные структуры с последующим выполнением ввода/вывода этих структур. Низлежащие vm_page_t, как правило, на время ввода/вывода становятся занятыми, что удобно с точки зрения кода драйвера файловой системы, которому нужно будет работать с буферами файловой системы, а не прямо со страницами VM..
FreeBSD резервирует ограниченное число KVM для хранения отображений из struct bufs, но должно быть понятно, что эти KVM используются строго для хранения отображений и не ограничивают возможности по кэшированию данных. Кэширование физических данных является исключительно функцией vm_page_t, а не буферов файловой системы. Однако, так как буферы файловой системы являются заменителями ввода/вывода, они соответственно ограничивают количество выполняемых одновременно операций ввода/вывода. Так как обычно доступно несколько тысяч буферов файловой системы, то это, как правило, проблем не вызывает.
Во FreeBSD таблица расположения физических страниц отделена от системы VM. Все жесткие таблицы страниц для каждого процесса могут быть перестроены на лету и обычно являются временными. Специальные таблицы страниц, например, те, что управляют KVM, обычно выделяются предварительно и перманентно. Эти таблицы страниц не являются временными.
FreeBSD связывает группы объектов vm_object с диапазонами адресов в виртуальной памяти посредством структур vm_map_t и vm_entry_t. Таблицы страниц строятся непосредственно из иерархии vm_map_t/vm_entry_t/ vm_object_t. Вернёмся к тому моменту, где я говорил о том, что физические страницы являются единственными, прямо связанными с vm_object. На самом деле это не совсем так. Объекты vm_page_t также объединяют в таблицы страниц и связывают с vm_object. Один vm_page_t может быть связан в несколько карт pmaps, так называются таблицы страниц. Однако иерархическая связь хранит все ссылки так, что одна и та же страница в том же самом объекте ссылается на одну и ту же vm_page_t, что дает нам универсальный кэширующий буфер.
FreeBSD использует KVM для хранения различных структур ядра. Самым большим единственным объектом, хранящимся в KVM, является кэширующий буфер файловой системы. Это отображения, связанные с объектами struct buf.
В отличие от Linux, FreeBSD не отображает всю физическую память в KVM. Это значит, что FreeBSD на 32-разрядных платформах может работать с конфигурациями памяти до 4ГБ. На самом деле, если бы аппаратный модуль управления памятью мог это делать, то на 32-разрядных платформах FreeBSD может теоретически работать с конфигурациями памяти до 8ТБ. Однако, так как большинство 32-разрядных платформ могут отображать только 4ГБ оперативной памяти, то этот вопрос остается теоретическим.
KVM управляется посредством нескольких методов. Основным механизмом, используемым для управления KVM, является zone allocator. Распределитель зоны берет кусок KVM и разделяет его на блоки памяти постоянного размера для того, чтобы выделить место под некоторый тип структуры. Вы можете воспользоваться командой vmstat -m для получения статистики текущего использования KVM до уровня зон.
Прилагались совместные усилия для того, чтобы сделать ядро FreeBSD
самонастраивающимся. Обычно вам не нужно разбираться ни с чем, кроме параметров
конфигурации ядра maxusers
и NMBCLUSTERS
. Это те параметры компиляции ядра, что указываются
(обычно) в файле /usr/src/sys/i386/conf/CONFIG_FILE. Описание всех доступных параметров
настройки ядра может быть найдено в файле /usr/src/sys/i386/conf/LINT.
В случае большой системы вам может понадобиться увеличить значение maxusers
. Значения этого параметра, как правило, располагаются в
диапазоне от 10 до 128. Заметьте, что слишком большое значение maxusers
может привести к переполнению доступной KVM, что влечет за
собой непредсказуемые результаты. Лучше задать некоторое разумное значение maxusers
и добавить другие параметры, такие, как NMBCLUSTERS
, для увеличения конкретных ресурсов.
Если ваша система будет интенсивно работать с сетью, вам может потребоваться увеличить
значение NMBCLUSTERS
. Обычно значения этого параметра
находятся в пределах от 1024 до 4096.
Параметр NBUF традиционно использовался для масштабирования системы. Этот параметр определяет количество KVA, которое может использоваться системой для отображения буферов файловой системы для ввода/вывода. Заметьте, что этот параметр не имеет никакого отношения с единым кэшируемым буфером! Этот параметр динамически настраивается в ядрах версии 3.0-CURRENT и более поздних и не должен изменяться вручную. Мы рекомендуем вам НЕ пытаться задавать значение параметра NBUF. Позвольте сделать это системе. Слишком маленькое значение может привести к очень низкой эффективности операций с файловой системой, когда как слишком большое значение может истощить очереди страниц, так как слишком много страниц окажутся связанными.
По умолчанию ядра FreeBSD не оптимизированы. Вы можете задать флаги отладки и
оптимизации при помощи директивы makeoptions в файле
конфигурации ядра. Заметьте, что вы не должны использовать параметр -g
, если не сможете использовать получающиеся при этом большие ядра
(обычно превышающие размером 7МБ).
makeoptions DEBUG="-g" makeoptions COPTFLAGS="-O -pipe"
Утилита sysctl дает возможность изменить параметры ядра во время его работы. Как правило, вам не приходится работать ни с какими sysctl-переменными, особенно с теми, что относятся к VM.
Тонкая оптимизация VM и системы во время работы сравнительно проста и понятна. Сначала включите использование Soft Updates на ваших файловых системах UFS/FFS везде, где это возможно. В файле /usr/src/contrib/sys/softupdates/README.softupdates находятся инструкции (и ограничения) по настройке этой функциональности.
Затем выделите достаточное количество пространства в области подкачки. Вы должны иметь на каждом физическом диске до четырех настроенных разделов подкачки, даже на ''рабочих'' дисках. Пространства в области подкачки должно быть не менее чем в два раза больше объема оперативной памяти, и даже больше, если памяти у вас не очень много. Вы должны также определять размер раздела подкачки, исходя из максимального объема оперативной памяти, который может быть на вашей машине, чтобы потом не выполнять разбиение диска повторно. Если вы хотите иметь возможность работы с дампом аварийного останова, то ваш первый раздел подкачки должен иметь объем, по крайней мере равный объему оперативной памяти, а в каталоге /var/crash должно быть достаточно свободного места для размещения дампа.
Подкачка поверх NFS прекрасно работает на системах версий 4.x и более поздних, но вы должны иметь в виду, что нагрузка от подкачки страниц ляжет на сервер NFS.
Эта глава является кратким введением в процесс написания драйверов устройств для FreeBSD. В этом контексте термин устройство используется в основном для вещей, связанных с оборудованием, относящимся к системе, таких, как диски, печатающие устройства или графические дисплеи с клавиатурами. Драйвер устройства является программной компонентой операционной системы, управляющей некоторым устройством. Имеются также так называемые псевдо-устройства, в случае которых драйвер устройства эмулирует поведение устройства программно, без наличия какой-либо соответствующей аппаратуры. Драйверы устройств могут быть скомпилированы в систему статически или могут загружаться по требованию при помощи механизма динамического компоновщика ядра `kld'.
Большинство устройств в UNIX-подобной операционной системе доступны через файлы устройств (device-nodes), иногда также называемые специальными файлами. В иерархии файловой системы эти файлы обычно находятся в каталоге /dev. В версиях FreeBSD, более старых, чем 5.0-RELEASE, в которых поддержка devfs(5) не интегрирована в систему, каждый файл устройства должен создаваться статически и вне зависимости от наличия соответствующего драйвера устройства. Большинство файлов устройств в системе создаются при помощи команды MAKEDEV.
Драйверы устройств могут быть условно разделены на две категории; драйверы символьных и сетевых устройств.
Интерфейс kld позволяет системным администраторам динамически добавлять и убирать функциональность из работающей системы. Это позволяет разработчикам драйверов устройств загружать собственные изменения в работающее ядро без постоянных перезагрузок для тестирования изменений.
Для работы с интерфейсом kld используются следующие команды привилегированного режима:
kldload - загружает новый модуль ядра
kldunload - выгружает модуль ядра
kldstat - выводит список загруженных в данный момент модулей
Скелет модуля ядра
/* * KLD Skeleton * Inspired by Andrew Reiter's Daemonnews article */ #include <sys/types.h> #include <sys/module.h> #include <sys/systm.h> /* uprintf */ #include <sys/errno.h> #include <sys/param.h> /* defines used in kernel.h */ #include <sys/kernel.h> /* types used in module initialization */ /* * Load handler that deals with the loading and unloading of a KLD. */ static int skel_loader(struct module *m, int what, void *arg) { int err = 0; switch (what) { case MOD_LOAD: /* kldload */ uprintf("Skeleton KLD loaded.\n"); break; case MOD_UNLOAD: uprintf("Skeleton KLD unloaded.\n"); break; default: err = EINVAL; break; } return(err); } /* Declare this module to the rest of the kernel */ static moduledata_t skel_mod = { "skel", skel_loader, NULL }; DECLARE_MODULE(skeleton, skel_mod, SI_SUB_KLD, SI_ORDER_ANY);
Во FreeBSD имеются заготовки для включения в make-файлы, которые вы можете использовать для быстрой компиляции собственных дополнений к ядру.
SRCS=skeleton.c KMOD=skeleton .include <bsd.kmod.mk>
Простой запуск команды make с этим make-файлом приведет к созданию файла skeleton.ko, который можно загрузить в вашу систему, набрав:
# kldload -v ./skeleton.ko
UNIX дает некоторый общий набор системных вызовов для использования в пользовательских приложениях. Когда пользователь обращается к файлу устройства, высокие уровни ядра перенаправляют эти обращения к соответствующему драйверу устройства. Скрипт /dev/MAKEDEV создает большинство файлов устройств в вашей системе, однако если вы ведете разработку своего собственного драйвера, то может появиться необходимость в создании собственных файлов устройств при помощи команды mknod.
Для создания файла устройства команде mknod требуется указать четыре аргумента. Вы должны указать имя файла устройства, тип устройства, старшее число устройства и младшее число устройства.
Файловая система устройств, devfs, предоставляет доступ к пространству имен устройств ядра из глобального пространства имен файловой системы. Это устраняет потенциальную проблемы наличия драйвера без статического файла устройства или файла устройства без установленного драйвера устройства. Devfs все еще находится в разработке, однако она уже достаточно хорошо работает.
Драйвер символьного устройства передает данные непосредственно в или из процесса пользователя. Это самый распространенный тип драйвера устройства и в дереве исходных текстов имеется достаточно простых примеров таких драйверов.
В этом простом примере псевдо-устройство запоминает какие угодно значения, которые вы в него записываете, и затем может выдавать их назад при чтении из этого устройства. Приведены две версии, одна для FreeBSD 4.X, а другая для FreeBSD 5.X.
Пример 7-1. Пример драйвера псевдо-устройства Echo для FreeBSD 4.X
/* * Simple `echo' pseudo-device KLD * * Murray Stokely */ #define MIN(a,b) (((a) < (b)) ? (a) : (b)) #include <sys/types.h> #include <sys/module.h> #include <sys/systm.h> /* uprintf */ #include <sys/errno.h> #include <sys/param.h> /* defines used in kernel.h */ #include <sys/kernel.h> /* types used in module initialization */ #include <sys/conf.h> /* cdevsw struct */ #include <sys/uio.h> /* uio struct */ #include <sys/malloc.h> #define BUFFERSIZE 256 /* Function prototypes */ d_open_t echo_open; d_close_t echo_close; d_read_t echo_read; d_write_t echo_write; /* Character device entry points */ static struct cdevsw echo_cdevsw = { echo_open, echo_close, echo_read, echo_write, noioctl, nopoll, nommap, nostrategy, "echo", 33, /* reserved for lkms - /usr/src/sys/conf/majors */ nodump, nopsize, D_TTY, -1 }; struct s_echo { char msg[BUFFERSIZE]; int len; } t_echo; /* vars */ static dev_t sdev; static int len; static int count; static t_echo *echomsg; MALLOC_DECLARE(M_ECHOBUF); MALLOC_DEFINE(M_ECHOBUF, "echobuffer", "buffer for echo module"); /* * This function is called by the kld[un]load(2) system calls to * determine what actions to take when a module is loaded or unloaded. */ static int echo_loader(struct module *m, int what, void *arg) { int err = 0; switch (what) { case MOD_LOAD: /* kldload */ sdev = make_dev(&echo_cdevsw, 0, UID_ROOT, GID_WHEEL, 0600, "echo"); /* kmalloc memory for use by this driver */ MALLOC(echomsg, t_echo *, sizeof(t_echo), M_ECHOBUF, M_WAITOK); printf("Echo device loaded.\n"); break; case MOD_UNLOAD: destroy_dev(sdev); FREE(echomsg,M_ECHOBUF); printf("Echo device unloaded.\n"); break; default: err = EINVAL; break; } return(err); } int echo_open(dev_t dev, int oflags, int devtype, struct proc *p) { int err = 0; uprintf("Opened device \"echo\" successfully.\n"); return(err); } int echo_close(dev_t dev, int fflag, int devtype, struct proc *p) { uprintf("Closing device \"echo.\"\n"); return(0); } /* * The read function just takes the buf that was saved via * echo_write() and returns it to userland for accessing. * uio(9) */ int echo_read(dev_t dev, struct uio *uio, int ioflag) { int err = 0; int amt; /* How big is this read operation? Either as big as the user wants, or as big as the remaining data */ amt = MIN(uio->uio_resid, (echomsg->len - uio->uio_offset > 0) ? echomsg->len - uio->uio_offset : 0); if ((err = uiomove(echomsg->msg + uio->uio_offset,amt,uio)) != 0) { uprintf("uiomove failed!\n"); } return err; } /* * echo_write takes in a character string and saves it * to buf for later accessing. */ int echo_write(dev_t dev, struct uio *uio, int ioflag) { int err = 0; /* Copy the string in from user memory to kernel memory */ err = copyin(uio->uio_iov->iov_base, echomsg->msg, MIN(uio->uio_iov->iov_len,BUFFERSIZE)); /* Now we need to null terminate */ *(echomsg->msg + MIN(uio->uio_iov->iov_len,BUFFERSIZE)) = 0; /* Record the length */ echomsg->len = MIN(uio->uio_iov->iov_len,BUFFERSIZE); if (err != 0) { uprintf("Write failed: bad address!\n"); } count++; return(err); } DEV_MODULE(echo,echo_loader,NULL);
Пример 7-2. Пример драйвера псевдо-устройства Echo для FreeBSD 5.X
/* * Simple `echo' pseudo-device KLD * * Murray Stokely * * Converted to 5.X by Sren (Xride) Straarup */ #include <sys/types.h> #include <sys/module.h> #include <sys/systm.h> /* uprintf */ #include <sys/errno.h> #include <sys/param.h> /* defines used in kernel.h */ #include <sys/kernel.h> /* types used in module initialization */ #include <sys/conf.h> /* cdevsw struct */ #include <sys/uio.h> /* uio struct */ #include <sys/malloc.h> #define BUFFERSIZE 256 /* Function prototypes */ static d_open_t echo_open; static d_close_t echo_close; static d_read_t echo_read; static d_write_t echo_write; /* Character device entry points */ static struct cdevsw echo_cdevsw = { .d_version = D_VERSION, .d_open = echo_open, .d_close = echo_close, .d_read = echo_read, .d_write = echo_write, .d_name = "echo" }; typedef struct s_echo { char msg[BUFFERSIZE]; int len; } t_echo; /* vars */ static struct cdev *echo_dev; static int count; static t_echo *echomsg; MALLOC_DECLARE(M_ECHOBUF); MALLOC_DEFINE(M_ECHOBUF, "echobuffer", "buffer for echo module"); /* * This function is called by the kld[un]load(2) system calls to * determine what actions to take when a module is loaded or unloaded. */ static int echo_loader(struct module *m, int what, void *arg) { int err = 0; switch (what) { case MOD_LOAD: /* kldload */ echo_dev = make_dev(&echo_cdevsw, 0, UID_ROOT, GID_WHEEL, 0600, "echo"); /* kmalloc memory for use by this driver */ echomsg = malloc(sizeof(t_echo), M_ECHOBUF, M_WAITOK); printf("Echo device loaded.\n"); break; case MOD_UNLOAD: destroy_dev(echo_dev); free(echomsg, M_ECHOBUF); printf("Echo device unloaded.\n"); break; default: err = EINVAL; break; } return(err); } static int echo_open(struct cdev *dev, int oflags, int devtype, struct thread *p) { int err = 0; uprintf("Opened device \"echo\" successfully.\n"); return(err); } static int echo_close(struct cdev *dev, int fflag, int devtype, struct thread *p) { uprintf("Closing device \"echo.\"\n"); return(0); } /* * The read function just takes the buf that was saved via * echo_write() and returns it to userland for accessing. * uio(9) */ static int echo_read(struct cdev *dev, struct uio *uio, int ioflag) { int err = 0; int amt; /* * How big is this read operation? Either as big as the user wants, * or as big as the remaining data */ amt = MIN(uio->uio_resid, (echomsg->len - uio->uio_offset > 0) ? echomsg->len - uio->uio_offset : 0); if ((err = uiomove(echomsg->msg + uio->uio_offset,amt,uio)) != 0) { uprintf("uiomove failed!\n"); } return(err); } /* * echo_write takes in a character string and saves it * to buf for later accessing. */ static int echo_write(struct cdev *dev, struct uio *uio, int ioflag) { int err = 0; /* Copy the string in from user memory to kernel memory */ err = copyin(uio->uio_iov->iov_base, echomsg->msg, MIN(uio->uio_iov->iov_len,BUFFERSIZE - 1)); /* Now we need to null terminate, then record the length */ *(echomsg->msg + MIN(uio->uio_iov->iov_len,BUFFERSIZE - 1)) = 0; echomsg->len = MIN(uio->uio_iov->iov_len,BUFFERSIZE); if (err != 0) { uprintf("Write failed: bad address!\n"); } count++; return(err); } DEV_MODULE(echo,echo_loader,NULL);
Для установки этого драйвера во FreeBSD 4.X сначала вам нужно создать файл устройства в вашей файловой системе по команде типа следующей:
# mknod /dev/echo c 33 0
Когда этот драйвер загружен, вы можете выполнять следующие действия:
# echo -n "Test Data" > /dev/echo # cat /dev/echo Test Data
Устройства, обслуживающие реальное оборудование, описываются в следующей главе.
Дополнительные источники информации
Учебник по программированию механизма динамического компоновщика ядра (KLD) - Daemonnews Октябрь 2000
Как писать драйверы ядра в парадигме NEWBUS - Daemonnews Июль 2000
Другие UNIX-системы могут поддерживать со вторым типом дисковых устройств, так называемых устройств с блочной организацией. Блочные устройства являются дисковыми устройствами, для которых ядро организует кэширование. Такое кэширование делает блочные устройства практически бесполезными, или по крайней мере ненадёжными. Кэширование изменяет последовательность операций записи, лишая приложение возможности узнать реальное содержимое диска в любой момент времени. Это делает предсказуемое и надежное восстановление данных на диске (файловые системы, базы данных и прочее) после сбоя невозможным. Так как запись может быть отложенной, то нет способа сообщить приложению, при выполнении какой именно операции записи ядро встретилось с ошибкой, что таким образом осложняет проблему целостности данных. По этой причине серьёзные приложения не полагаются на блочные устройства, и, на самом деле практически во всех приложениях, которые работают с диском напрямую, имеется большая проблема выбора устройств с последовательным доступом (или ''raw''), которые должны использоваться. Из-за реализации отображения каждого диска (раздела) в два устройства с разными смыслами, которая усложняет соответствующий код ядра, во FreeBSD поддержка дисковых устройств с кэшированием была отброшена в процессе модернизации инфраструктуры I/O-операций с дисками.
В случае драйверов сетевых устройств файлы устройств для доступа к ним не используются. Их выбор основан на другом механизме, работающем в ядре, и не использующем вызов open(); об использование сетевых устройств в общем случае рассказано в описании системного вызова socket(2).
За дополнительной информацией смотрите: ifnet(9), исходный код петлевого устройства ( loopback ) , сетевые драйверы Билла Пола (Bill Paul).
Эта глава посвящена механизмам FreeBSD по написанию драйверов устройств, работающих на шине PCI.
Здесь находится информация о том, как код шины PCI проходит по не подключенным устройствам и распознает возможность загруженного драйвера kld выполнить подключение к какому-либо из них.
/* * Simple KLD to play with the PCI functions. * * Murray Stokely */ #define MIN(a,b) (((a) < (b)) ? (a) : (b)) #include <sys/param.h> /* defines used in kernel.h */ #include <sys/module.h> #include <sys/systm.h> #include <sys/errno.h> #include <sys/kernel.h> /* types used in module initialization */ #include <sys/conf.h> /* cdevsw struct */ #include <sys/uio.h> /* uio struct */ #include <sys/malloc.h> #include <sys/bus.h> /* structs, prototypes for pci bus stuff */ #include <machine/bus.h> #include <sys/rman.h> #include <machine/resource.h> #include <dev/pci/pcivar.h> /* For get_pci macros! */ #include <dev/pci/pcireg.h> /* Function prototypes */ d_open_t mypci_open; d_close_t mypci_close; d_read_t mypci_read; d_write_t mypci_write; /* Character device entry points */ static struct cdevsw mypci_cdevsw = { .d_open = mypci_open, .d_close = mypci_close, .d_read = mypci_read, .d_write = mypci_write, .d_name = "mypci", }; /* vars */ static dev_t sdev; /* We're more interested in probe/attach than with open/close/read/write at this point */ int mypci_open(dev_t dev, int oflags, int devtype, d_thread_t *td) { int err = 0; printf("Opened device \"mypci\" successfully.\n"); return(err); } int mypci_close(dev_t dev, int fflag, int devtype, struct d_thread_t *td) { int err = 0; printf("Closing device \"mypci.\"\n"); return(err); } int mypci_read(dev_t dev, struct uio *uio, int ioflag) { int err = 0; printf("mypci read!\n"); return err; } int mypci_write(dev_t dev, struct uio *uio, int ioflag) { int err = 0; printf("mypci write!\n"); return(err); } /* PCI Support Functions */ /* * Return identification string if this is device is ours. */ static int mypci_probe(device_t dev) { device_printf(dev, "MyPCI Probe\nVendor ID : 0x%x\nDevice ID : 0x%x\n", pci_get_vendor(dev), pci_get_device(dev)); if (pci_get_vendor(dev) == 0x11c1) { printf("We've got the Winmodem, probe successful!\n"); return (0); } return (ENXIO) } /* Attach function is only called if the probe is successful */ static int mypci_attach(device_t dev) { printf("MyPCI Attach for : deviceID : 0x%x\n",pci_get_vendor(dev)); sdev = make_dev(&mypci_cdevsw, 0, UID_ROOT, GID_WHEEL, 0600, "mypci"); printf("Mypci device loaded.\n"); return (ENXIO); } /* Detach device. */ static int mypci_detach(device_t dev) { printf("Mypci detach!\n"); return (0); } /* Called during system shutdown after sync. */ static int mypci_shutdown(device_t dev) { printf("Mypci shutdown!\n"); return (0); } /* * Device suspend routine. */ static int mypci_suspend(device_t dev) { printf("Mypci suspend!\n"); return (0); } /* * Device resume routine. */ static int mypci_resume(device_t dev) { printf("Mypci resume!\n"); return (0); } static device_method_t mypci_methods[] = { /* Device interface */ DEVMETHOD(device_probe, mypci_probe), DEVMETHOD(device_attach, mypci_attach), DEVMETHOD(device_detach, mypci_detach), DEVMETHOD(device_shutdown, mypci_shutdown), DEVMETHOD(device_suspend, mypci_suspend), DEVMETHOD(device_resume, mypci_resume), { 0, 0 } }; static driver_t mypci_driver = { "mypci", mypci_methods, 0, /* sizeof(struct mypci_softc), */ }; static devclass_t mypci_devclass; DRIVER_MODULE(mypci, pci, mypci_driver, mypci_devclass, 0, 0);
Дополнительная информация
PCI System Architecture, Fourth Edition by Tom Shanley, et al.
FreeBSD предоставляет объектно-ориентированный механизм для запроса ресурсов от родительской шины. Почти все устройства будут находиться в качестве ребёнка не важно какой шины (PCI, ISA, USB, SCSI и т.д) и нуждаться в приобретении ресурсов от родительской шины таких, как сегменты памяти, линии прерываний, DMA каналы.
Чтобы сделать что-либо полезное с PCI устройством вам надо получить доступ к базовым адресным регистрам (Base Adress
Registres (BARs) ) области конфигурации PCI. PCI специфичные детали получения BAR
абстрагированы в функции bus_alloc_resource()
.
К примеру, типичный драйвер должен иметь что-то похожее в функции attach()
:
sc->bar0id = PCI_BAR(0); sc->bar0res = bus_alloc_resource(dev, SYS_RES_MEMORY, &(sc->bar0id), 0, ~0, 1, RF_ACTIVE); if (sc->bar0res == NULL) { printf("Memory allocation of PCI base register 0 failed!\n"); error = ENXIO; goto fail1; } sc->bar1id = PCI_BAR(1); sc->bar1res = bus_alloc_resource(dev, SYS_RES_MEMORY, &(sc->bar1id), 0, ~0, 1, RF_ACTIVE); if (sc->bar1res == NULL) { printf("Memory allocation of PCI base register 1 failed!\n"); error = ENXIO; goto fail2; } sc->bar0_bt = rman_get_bustag(sc->bar0res); sc->bar0_bh = rman_get_bushandle(sc->bar0res); sc->bar1_bt = rman_get_bustag(sc->bar1res); sc->bar1_bh = rman_get_bushandle(sc->bar1res);
Дескрипторы каждого базового регистра хранятся в структуре softc
и в дальнейшем могут быть использованы для записи в
устройство.
С помощью функций bus_space_*
эти дескрипторы могут быть
использованы для чтения или записи в регистры устройства. Например, в драйвере может быть
реализована функция чтения данных из определенного регистра устройства:
uint16_t board_read(struct ni_softc *sc, uint16_t address) { return bus_space_read_2(sc->bar1_bt, sc->bar1_bh, address); }
Похожим способом делается запись в регистры:
void board_write(struct ni_softc *sc, uint16_t address, uint16_t value) { bus_space_write_2(sc->bar1_bt, sc->bar1_bh, address, value); }
Эти функции существуют в 8, 16, 32 битных версиях. Используйте bus_space_{read|write}_{1|2|4}
соответственно.
Прерывания (IRQ) распределяются с помощью объектно-ориентированного кода шины способом, похожим на способ распределения ресурсов памяти. Сначала IRQ ресурс выделяется от родительской шины, затем настраивается обработчик прерываний для связи с этим IRQ.
Пример функции attach()
скажет больше чем слова.
/* Get the IRQ resource */ sc->irqid = 0x0; sc->irqres = bus_alloc_resource(dev, SYS_RES_IRQ, &(sc->irqid), 0, ~0, 1, RF_SHAREABLE | RF_ACTIVE); if (sc->irqres == NULL) { printf("IRQ allocation failed!\n"); error = ENXIO; goto fail3; } /* Now we should set up the interrupt handler */ error = bus_setup_intr(dev, sc->irqres, INTR_TYPE_MISC, my_handler, sc, &(sc->handler)); if (error) { printf("Couldn't set up irq\n"); goto fail4; } sc->irq_bt = rman_get_bustag(sc->irqres); sc->irq_bh = rman_get_bushandle(sc->irqres);
Осторожно отсоединяйте программу драйвера. Вы должны успокоить поток прерываний
устройства и удалить дескриптор прерывания. После выполнения функции bus_teardown_intr()
ваш дескриптор прерываний не будет больше
вызываться и все треды, которые возможно могли использовать этот дескриптор завершились.
Так как эта функция может "засыпать", то вы не должны удерживать мьютексы при вызове этой
функции.
Это глава является устаревшей и существует только по историческим причинам. Для работы
с данной возможностью используйте семейство функций bus_space_dma*()
. Этот параграф может будет убран при обновлении
этой главы. На данный момент в API вносятся изменения, и когда все вопросы будут решены
было бы неплохо обновить эту главу.
На ПК, периферийные устройства желающие использовать возможности DMA должны работать с
физическими адресами, что является проблемой, так как FreeBSD использует виртуальную
память и почти полностью имеет дело с виртуальными адресами. К счастью, существует
функция vtophys()
способная помочь.
#include <vm/vm.h> #include <vm/pmap.h> #define vtophys(virtual_address) (...)
Немного по другому решается данная проблема на платформе alpha. В этой ситуации
хорошим решением будет использование макроопределения vtobus()
.
#if defined(__alpha__) #define vtobus(va) alpha_XXX_dmamap((vm_offset_t)va) #else #define vtobus(va) vtophys(va) #endif
Очень важно освободить все реcурсы, выделенные с помощью attach()
. Осторожно освобождайте корректные вещи даже на отказных
условиях, чтобы система оставалась в рабочем состоянии, пока ваш драйвер не умрет.
Универсальная Последовательная Шина (Universal Serial Bus - USB) является новым способом подключения устройств к персональным компьютерам. Среди возможностей архитектуры шины имеется двунаправленный обмен данными и это было разработано в качестве ответа на то, что устройства становятся все более сложными и требуют большего взаимодействия с хостом. Поддержка USB включена во все современные наборы микросхем для PC и поэтому имеется во всех недавно выпущенных PC. Выпуск компанией Apple компьютера iMac только с USB стал большим знаком для производителей оборудования на создание USB-версий своих устройств. Спецификации будущие PC задают, что все устаревшие разъемы на PC должны быть заменены на один или несколько разъемов USB, что дает всеобщие возможности технологии plug and play. Поддержка оборудования USB имелась в начальном объеме в NetBSD и была разработана Леннартом Ангустссоном (Lennart Augustsson) для проекта NetBSD. Код был перенесен во FreeBSD и в настоящее время мы поддерживаем общий код. Для реализации подсистемы USB важны несколько возможностей USB.
Lennart Augustsson сделал большую часть работы в реализации поддержки USB для проекта NetBSD. Приносим много благодарностей за эту исключительную работу. большое спасибо также Арди (Ardy) и Дирку (Dirk) за их комментарии и выверку текста этого документа.
Устройства подключаются непосредственно к портам компьютера или к устройствам, которые называются разветвителями, что формирует структуру устройств, похожую на дерево.
Устройства могут подключаться и отключаться во время работы.
Устройства могут самостоятельно переходить в режим ожидания или получать сигналы на продолжение работы от ведущей системы
Так как устройства могут получать электропитание с шины, то программное обеспечение хоста отслеживает напряжение для каждого разветвителя.
Различное качество обслуживания, требуемое различными типами устройств, вместе с максимальным их количеством в 126 устройств, которые можно подключить к одной и той же шине, требует тщательного планирования передачи данных по общей шине для получения полной отдачи от общей пропускной способности в 12Мбит/c. (более 400Мбит/c для USB 2.0)
Устройства сложны и содержат легкодоступную информацию о самих себе
Разработка драйверов для подсистемы USB и устройств, к ней подключаемых, поддерживается разрабатываемыми спецификациями и спецификациями, которые будут разработаны. Эти спецификации общедоступны с домашних страниц USB. Apple сильно продвигает драйверы, основанные на стандартах, помещая в свободный доступ драйверы классов общего назначения для своей операционной системы MacOS и не рекомендует использование отдельных драйверов для каждого нового устройства. В этой главе делается попытка собрать информацию, достаточную для общего понимания текущей реализации набора драйверов USB для FreeBSD/NetBSD. Но все же рекомендуется дополнительно прочитать и соответствующие спецификации, упоминаемые ниже.
Поддержка USB во FreeBSD может быть разделена на три слоя. Самый нижний слой содержит драйвер контроллера хоста, который дает общий интерфейс к оборудованию и его возможностям по управлению. Он поддерживает инициализацию оборудования, управление передачами и обработку полных и/или прерванных передач. Любой драйвер контроллера хоста реализует виртуальный разветвитель, который дает независимый от оборудования доступ к регистрам, контролирующим корневые порты на задней панели машины.
Средний слой обрабатывает подключение и отключение устройства, основную инициализацию устройства, выбор драйвера, каналы связи и управление ресурсами. Этот слой услуг также управляет каналами по умолчанию и запросы устройств, передаваемых по этим каналам.
Верхний уровень содержит отдельные драйверы, поддерживающие специфичные (классы) устройств. Эти драйверы реализуют протокол, который используется в каналах, отличающихся от используемых по умолчанию. Они также реализуют дополнительную функциональность, которая делает устройство доступным другим частям ядра или пользовательской части. Они используют интерфейс драйвера USB (USBDI), используемый слоем сервисов.
Хост-контроллер (HC) управляет передачей пакетов по шине. Использовались кадры в 1 миллисекунду. В начале каждого кадра хост-контроллер генерирует пакет начала кадра (SOF - Start of Frame).
Пакет SOF используется для синхронизации начала кадра и отслеживания количества кадров. Пакеты передаются с каждым кадром, как от хоста к устройству (исходящие), так и от устройства к хосту (входящие). Передачи всегда инициируются хостом (запрошенные передачи). В силу этого может быть только один хост на шине USB. Каждая передача пакета имеет период статуса, в котором сторона, принимающая данные, может возвратить ACK (подтверждение приема), NAK (повтор), STALL (условная ошибка) или ничего (потерянный период данных, недоступное устройство или отсоединение). Раздел 8.5 Спецификации USB детально описывает пакеты. На шине USB могут произойти четыре различных типа передач: управляющая, основная, прерывание и изохронная. Типы передач и их характеристики описаны ниже (подраздел `Каналы').
Передачи больших объемов данных между устройством на шине USB и драйвером устройства делятся на множество пакетов хост-контроллером или драйвером HC.
Запросы устройства (управляющие передачи) к конечных точкам, используемым по умолчанию, являются специальными. Они состоят из двух или трех фаз: SETUP, DATA (oпциональная) и STATUS. пакет посылается устройству. Если есть фаза данных, то направление пакетов (или пакета) данных дается в настроечном пакете. Направление в фазе статуса противоположно направлению во время фазы данных. или IN если не было фазы данных. Оборудование хост-контроллера также дает регистры с текущим статусом корневых портов и изменений, которые случились с момента последнего сброса регистра изменения статуса. Доступ к этим регистрам дается через виртуализированных разветвитель, как и предполагается по спецификации USB [ 2]. Виртуальный разветвитель должен работать вместе с классом устройств-разветвителей, который описывается в 11 главе той спецификации. Он должен давать канал, используемый по умолчанию, через который запросы устройств могут ему посылаться. Он возвращает набор дескрипторов, стандартных и специфичных для класса разветвителя. Он должен также давать канал прерываний, который сообщает об изменениях, произошедших на его портах. На данный момент для хост-контроллеров существуют две спецификации: Universal Host Controller Interface (UHCI; Intel) и Open Host Controller Interface (OHCI; Compaq, Microsoft, National Semiconductor). Спецификация UHCI разработана для уменьшения аппаратной сложности, требуя от драйвера хост-контроллера поддержки полного распределения передач для каждого кадра. Контроллеры типа OHCI гораздо более независимы, и дают более абстрактный интерфейс, выполняя много работы самостоятельно.
Хост-контроллер UHCI отслеживает список кадров с 1024 указателями на структуры данных, соответствующих отдельному фрейму. Он понимает два различных типа данных: описатели передач (TD - transfer descriptor) и начала очереди (QH - queue heads). Каждый TD представляет пакет, связывающий от или в конечную точку устройства. QH имеют смысл для объединения TD (и QH) вместе.
Каждая передача состоит из одного или большего количества пакетов. Драйвер UHCI разделяет большие объемы передач на множество пакетов. Для каждой передачи, за исключением изохронных передач, формируется QH. Для каждого типа передачи эти QH объединяются в QH для этого типа. Изохронные передачи выполняются в первую очередь из-за фиксированных требований к устойчивости и непосредственно ссылается по указателю на список кадров. Последний изохронный TD ссылается на QH для передачи прерываний для этого кадра. Все QH для передач прерываний указывают на QH для управляющих передач, которые, в свою очередь, указывают на QH для основных передач. Следующая диаграмма дает графическое представление этого:
Это приводит к следующему сценарию, запускаемому в каждом кадре. После получения указателя на текущий кадр из списка кадров контроллер сначала выполняет TD для всех изохронных пакетов в этом кадре. Последний из этих TD ссылается на QH для передач прерываний для этого кадра. Хост-контроллер затем спускается от этого QH к QH для отдельных передач прерываний. После завершения работы этой очереди QH для прерванных передач будет отсылать контроллер на QH для всех управляющих передач. Он будет выполнять все под-очереди, здесь запланированные, за которыми следуют все передачи, поставленные в очередь в массовые QH. Для облегчения обработки законченных или завершившихся неудачно передач аппаратурой генерируется различные типы прерываний в конце каждого кадра. В последнем TD для передачи бит Interrupt-On Completion (прерывание при завершении) устанавливается драйвером HC для вызова прерывания после окончания передачи. Прерывание ошибки устанавливается, если TD достиг своего максимального количества ошибок. Если в TD установлен бит обнаружения короткого пакета, и передается пакет, размером меньшим, чем установлено, устанавливается это прерывание для оповещения драйвера контроллера о завершении передачи. Задачей драйвера хост-контроллера является нахождение того, какая передача была завершена или выдача ошибки. При вызове прерывания вспомогательная подпрограмма найдет все завершенные передачи и вызовет их подпрограммы.
Более детальное описание находится в спецификации на UHCI.
Программирование хост-контроллера OHCI гораздо проще. Контроллер полагает, что имеется набор конечных точек, и заботится о планировании приоритетов и порядке следования типов передач в кадре. Основной структурой данных, используемой хост-контроллером, является описатель конечной точки (Endpoint Descriptor - ED), которому назначается очередь описателей передач (Transfer Descriptors - TD). ED хранит максимальный размер пакета, разрешенный для конечной точки, а аппаратура контроллера выполняет разбиение на пакеты. Указатели на буферы данных обновляются после каждой передачи, и когда начальный и конечный указатели совпадут, TD отбрасывается в очередь выполненного. Четыре типа конечных точек имеют свои собственные очереди. Управляющие и обычные конечные точки ставятся каждая в свою собственную очередь. ED прерываний ставятся в очередь в дерево, с уровнем в дереве, задающим частоту, с которой они выполняются.
framelist interruptisochronous control bulk
Распределение, выполняемое хост-контроллером в каждом кадре, имеет следующий вид. Контроллер сначала выполняет очереди непериодичного управления и обычную очередь, до момента времени, устанавливаемого драйвером HC. Затем выполняются прерванные передачи для этого количества кадров, используя младшие пять бит номера кадра в качестве индекса в уровне 0 дерева прерываний ED. В конце этого дерева подключаются изохронные ED и они выполняются последовательно. Изохронные TD содержат номер первого кадра, с которого должна начаться передача. После выполнения всех периодических передач, снова обрабатываются управляющая и обычная очереди. Периодически вызывается подпрограмма обслуживания прерываний для обработки очереди выполненного и вызова соответствующих функций для каждой передачи и перепланирования изохронных конечных точек и прерываний.
Более детальное описание есть в спецификации OHCI. Средний уровень услуг дает доступ к устройству в смысле управления и отслеживает ресурсы, используемые различными драйверами и уровнями услуг. Уровень отвечает за следующие вопросы:
Информация о настройке устройства
Каналы коммуникаций с устройством
Распознавание, подключение и отключение от устройства.
Каждое устройство имеет различные уровни детализации информации о настройке. Каждое устройство имеет одну или несколько конфигураций, одна из которых выбирается во время инициализации/подключения. При выборе конфигурации определяются требования к электропитанию и пропускной способности. Каждой конфигурации может соответствовать несколько интерфейсов. Интерфейс устройства является набором конечных точек. К примеру, колонки USB могут иметь интерфейс для звуковых данных (Audio Class) и интерфейс для ручек, кнопок, шкал (HID Class). Все интерфейсы в конфигурации активны в одно и то же время и могут быть подключены к различным драйверам. Каждый интерфейс может иметь альтернативы, дающие различное качество вспомогательных параметров. К примеру, видеокамеры, используемые для задания различных размеров кадров и количества кадров в секунду.
Внутри каждого интерфейса может быть задано 0 или большее количество конечных точек. Конечные точки являются однонаправленными точками доступа для коммуникации с устройством. Они предоставляют буферы для временного хранения получаемых и передаваемых от устройства данных. Каждая конечная точка в конфигурации имеет уникальный адрес, номер конечной точки плюс ее направление. Конечная точка, используемая по умолчанию, конечная точка 0, не является частью никакого интерфейса и доступна во всех конфигурациях. Она управляется уровнями услуг и драйверам устройств непосредственно не доступна.
Level 0 Level 1 Level 2 Slot 0
Slot 3 Slot 2 Slot 1
(Показаны только 4 из 32 слотов)
Такая иерархически организованная информация о конфигурации описана в устройстве стандартным набором дескрипторов (обратитесь к разделу 9.6 спецификации USB [ 2]). Они могут быть получены через запрос на получение дескриптора (Get Descriptor Request). Слой услуг кэширует эти дескрипторы для избежания ненужных передач по шине USB. Доступ к дескрипторам дается через вызовы функций.
Дескрипторы устройства: Общая информация об устройстве, такая, как Производитель, Наименование продукта и Номер версии, поддерживаемый класс устройств, подкласс и протокол, если это имеет смысл, максимальный размер пакета для конечной точки, используемой по умолчанию, и так далее.
Дескрипторы конфигурации: Количество интерфейсов в этой конфигурации, поддержка функциональность приостанова и продолжения работы, а также требования по электропитанию.
Дескрипторы интерфейса: класс интерфейса, подкласс и протокол, если он уместен, количество альтернативных настроек для интерфейса и число конечных точек.
Дескрипторы конечной точки: Адрес конечной точки, ее направление и тип, максимальный поддерживаемый размер пакета и частота посылки запросов, если конечная точка имеет тип прерывание. Для конечной точки, используемой по умолчанию (конечная точка 0) дескриптора не существует и он никогда не считается как дескриптор интерфейса.
Строковые дескрипторы строк: В других дескрипторах для некоторых полей даются строковые индексы. Они могут использоваться для получения описательных строк, возможно, на нескольких языках.
Определения класса могут добавлять свои собственные типы дескрипторов, которые доступны через запрос на получение дескриптора.
Конвейерное общение с конечными точками устройства происходит через так называемые каналы. Драйверы отдают передачи к конечным точкам в канал и дают callback-функцию для вызова при окончании или сбое передачи (асинхронные передачи) или ожидают окончания (синхронные передачи). Передачи в конечную точку ставятся в очередь в канале. Передача может быть завершена, либо произойти с ошибкой либо остановиться по таймауту (если он был задан). Для передач существуют два типа таймаутов. Таймауты могут возникнуть из-за задержек на шине USB (миллисекунды). Эти задержки видятся как сбои и могут появиться по причине отключения устройства. Второй тип задержки реализован в программном обеспечении и появляется, когда передача не завершена в течение заданного периода времени (секунды). Они появляются, когда устройство отвечает отрицательно (NAK) на запросы передачи пакетов. Причиной этого является неготовность устройства к приему данных, переполнение или заполнение буфера или ошибки протокола.
Если передача по каналу превышает максимальный размер пакета, указанный в соответствующем дескрипторе конечной точки, то хост-контроллер (OHCI) или драйвер HC (UHCI) будет разбивать передачу на пакеты максимального размера, с последним пакетом, возможно, меньшим, чем максимальный размер пакета.
Иногда для устройства не является проблемой возвратить меньше данных, чем запрошено. К примеру, запрос на передачу от модема может запросить 200 байт данных, но модем сейчас имеет только 5 байт. Драйвер может задать признак короткого пакета (SPD). Это позволяет хост-контроллеру принять пакет, даже если количество передаваемых данных меньше, чем запрошено. Этот флаг имеет смысл только для входящих передач, так как объем пересылаемых устройству данных всегда известен заранее. Если в устройстве во время передачи произошла неисправимая ошибка, то канал теряется. Перед тем, как любые дополнительные данные будут подтверждены или переданы, драйверу нужно выяснить причину потери и сбросить условие потери конечной точки при помощи посылки запроса на очистку конечной точки и сброса устройства по каналу, используемому по умолчанию. Конечная точка, используемая по умолчанию, никогда не должна теряться.
Имеются четыре различных типа конечных точек и соответствующих каналов: - управляющий канал / канал, используемый по умолчанию: Имеется один управляющий канал на устройство, подключенный к конечной точке, используемой по умолчанию (конечная точка 0). Канал содержит запросы устройства и соответствующие данные. Различие между передачами по каналу, используемому по умолчанию и другими каналами заключается в том, что протокол для передач описан в спецификации USB [ 2]. Эти запросы используются для сброса и настройки устройства. Базовый набор команд, который должен поддерживаться всеми устройствами, дан в главе 9 спецификации USB [ 2]. Команды, поддерживаемые в этом канале, могут быть расширены спецификацией класса устройств для поддержки дополнительной функциональности.
Канал передачи потока: Это USB-эквивалент для среды передачи без обработки.
Канал прерываний: Хост посылает запрос на передачу данных устройству, и если в устройстве нет данных для передачи, она посылает NAK на пакет данных. Передачи прерываний планируются на частоте, задаваемой при создании канала.
Изохронный канал: Такие каналы предназначены для изохронных данных, к примеру, потоки аудио или видео, с фиксированной устойчивостью, но не с гарантированной доставкой. В текущей реализации имеются некоторая поддержка для каналов этого типа. Пакеты в управлении, данные и передачи прерываний выполняются повторно, если при передаче возникает ошибка или если устройство отвечает на подтверждение отрицательно (NAK), например, из-за нехватки места в буфере для хранения входящих данных. Изохронные пакеты, однако, не повторяются в случае сбоя при доставке или получения NAK пакета, так как это может нарушить ограничения по времени.
Доступность необходимой пропускной способности вычисляется во время создания канала. Передачи планируются в кадрах продолжительностью 1 миллисекунда. Выделение пропускной способности внутри кадра описано в спецификациях USB, раздел 5.6 [ 2]. Изохронным передачам и передачам прерываний разрешено использовать до 90% пропускной способности кадра. Пакеты для управления и обычных передач планируются после всех изохронных пакетов и пакетов прерываний и будут занимать всю оставшуюся пропускную способность.
Дополнительная информация о планировании передач и корректировке пропускной способности может быть найдена в главе 5 спецификаций USB [ 2], разделе 1.3 спецификации UHCI [ 3] и разделе 3.4.2 спецификации OHCI [4].
После оповещения от разветвителя о том, что было подключено новое устройство, уровень услуг включает порт, предоставляя устройству напряжение в 100 mA. В этот момент устройство находится в своем состоянии по умолчанию и слушает на адресе устройства 0. Уровень услуг будут продолжать запрашивать различные дескрипторы через канал по умолчанию. После этого он пошлет запрос установки адреса для перемещения устройства от адреса устройства, используемого по умолчанию (адрес 0). Несколько драйверов устройств могут поддерживать устройство. К примеру, драйвер модема может поддерживать ISDN TA через интерфейс, совместимый с AT. Драйвер для этой конкретной модели адаптера ISDN может, однако, дать улучшенную поддержку для этого этого устройства. Для обеспечения такой гибкости процедура распознания возвращает приоритеты, показывающие их уровень поддержки. Поддержка конкретной версии продукта дает драйверу общего вида наименьший приоритет. Может также быть, что несколько драйверов могут подключаться к одному устройству, если есть несколько интерфейсов в одной конфигурации. Каждому драйверу достаточно поддерживать только подмножество интерфейсов.
При распознании драйвера для только что подключенного устройства сначала проверяются драйверы для специфичных устройств. Если они не найдены, то код распознания проходит по всем поддерживаемым конфигурациям то тех пор, пока драйвер не подключит некоторую конфигурацию. Для поддержки устройств с несколькими драйверами на разных интерфейсах процедура распознания проходит по всем интерфейсам в конфигурации, которые еще не были заняты драйвером. Конфигурации, которые превысили ограничения на электропитание для хаба, игнорируются. Во время подключения драйвер должен инициализировать устройство в нормальное состояние, но не переинициализировать его, так как это приведет к отключению устройства от шины и перестартовать процесс распознания для него. Во избежание избыточного потребления пропускной способности во время подключения не нужно захватывать канал прерываний, но выделение канала должно быть отложено до открытия файла и реального использования данных. Когда файл закрывается, то канал должен быть снова закрыт, даже если устройство продолжает оставаться подключенным.
Драйвер устройства должен ожидать получение ошибок во время любого обмена данными с устройством. Архитектура USB поддерживает и поощряет отключение устройств в любой момент времени. При исчезновении устройства драйверы должны работать корректно.
Более того, устройство, которое было отключено и подключено повторно, не будет подключено повторно на тот же самый экземпляр устройства. Это может измениться в будущем, когда большее количество устройств будет поддерживать серийные номера (обратитесь к информации о дескрипторе устройства) или, другими словами, означает разработку идентификации для устройства.
Отключение устройства сообщается хабом в пакете прерываний, передаваемом драйвером хаба. Информация изменения статуса указывает, на каком порту обнаружено изменение. Вызывается метод отключения устройства для всех драйверов устройств для устройства, подключенного на этом порту, и очищаются структуры. Если статус порта указывает на то, что в данный момент устройство подключено на этом порту, то начинается процедура обнаружения и подключения устройства. Сброс устройства вызовет последовательность отключений-подключений на хабе и будет отработан так, как описано выше.
Протокол, используемый в каналах, отличающихся от канала, используемого по умолчанию, в спецификации USB не определен. Информация об этом может быть найдена в различных источниках. Самым надежным является раздел разработчиков на домашних страницах USB [ 1]. На этих страницах можно найти все возрастающее количество спецификаций классов устройств. Эти спецификации задают то, как должно выглядеть соответствующее стандарту устройство с точки зрения драйвера, базовую функциональность, которую оно должно предоставлять и протокол, который используется в коммуникационных каналах. Спецификация USB [ 2] включает описание класса концентратора USB. Спецификация класса для Устройств взаимодействия с человеком (HID - Human Interface Devices) был создан для работы с клавиатурами, планшетами, устройствами чтения бар-кода, кнопок, ручек, переключателей и так далее. Третьим примером является спецификация класса для устройств хранения информации. Полный список классов устройств можно найти в разделе разработчиков на домашних страницах USB [ 1].
Однако для многих устройств информация о протоколе еще не была опубликована. Информация об используемом протоколе может быть получена от компании, создавшей устройство. Некоторые компании будут требовать от вас подписания Соглашения о Неразглашении (NDA - Non-Disclosure Agreement) до того, как даст вам спецификации. Это в большинстве случаев исключает создание драйвера с открытым кодом.
Другим хорошим источником информации являются исходные коды драйверов Linux, потому что некоторое количество компаний начали предоставлять драйверы своих устройств для Linux. Всегда полезно связаться с авторами этих драйверов для выяснения их источника информации.
Пример: Устройства взаимодействия с человеком. На спецификации для этих устройств, таких, как клавиатуры, мыши, планшеты, кнопки, пульты и прочее, имеются ссылки в спецификациях других классов устройств, и они используются во многих устройствах.
К примеру, аудио-колонки дают конечные точки для цифровых и аналоговых преобразователей и, возможно, дополнительный канал для микрофона. Они также дают конечную точку HID в отдельном интерфейсе для кнопок и ручек на передней панели устройства. То же самое имеет место для класса управления монитором. Понятно, как строить поддержку для этих интерфейсов через имеющиеся пользовательские библиотеки и библиотеки ядра вместе с драйвером класса HID или драйвера общего вида. Другим устройством, которое может выступать примером для интерфейсов внутри одной конфигурации, управляемыми различными драйверами устройств, является дешевая клавиатура со встроенным устаревшим портом мыши. Во избежание повышения стоимости за счет включения оборудования для концентратора USB в устройство, производители объединяют данные мыши, получаемые с порта PS/2 на задней стенке клавиатуры и нажатия клавиш от клавиатуры, в два отдельных интерфейса одной и той же конфигурации. Драйверы мыши и клавиатуры каждый подключается к соответствующему интерфейсу и организуют каналы к двум независимым конечным точкам.
Пример: Загрузка прошивок. Многие уже разработанные устройства опираются на процессор общего назначения с дополнительным ядром USB. Так как разработка драйверов и прошивок для устройств USB все еще является новым делом, многие устройства после подключения требуют загрузки прошивок.
При этом выполняется следующая процедура. Устройство идентифицирует себя при помощи идентификаторов производителя и продукта. Первый драйвер распознает и подключает его, после чего загружает прошивку в устройство. После этого устройство программно сбрасывает себя и драйвер отключается. После короткой задержки устройство объявляет о своем присутствии на шине. Устройство меняет свои идентификаторы производителя/продукта/версии для отражения того факта, что в нем уже загружена прошивка, и соответственно второй драйвер будет распознавать и подключаться к устройству.
Примером такого типа устройств является плата ввода/вывода ActiveWire, построенная на наборе микросхем EZ-USB. Для этого набора имеется загрузчик прошивок общего вида. Прошивка, загруженная в адаптер ActiveWire, изменяет номер версии. Затем выполняется программный сброс микросхемы EZ-USB в части USB для отключения от шины USB и повторного подключения.
Пример: Устройства хранения данных. Поддержка устройств хранения данных главным образом строится на основе существующих протоколов. Накопитель Iomega USB Zipdrive основан на SCSI-версии их устройства. Команды SCSI и сообщения о состоянии объединяются в блоки и передаются по каналам передачи данных к устройству и от устройства, эмулируя контроллер SCSI по соединению USB. Команды ATAPI и UFI поддерживаются подобным же образом.
Спецификация на устройства хранения поддерживает 2 различных типа объединения блоков управления. Первоначальная попытка была основана на посылке команды и состояния через канал, используемый по умолчанию, и использовании основных передач для данных, перемещаемых между хостом и устройством. На основании опыта работы второй подход был разработан на основе объединения блоков команд и статуса и посылке их по основному каналу к и от конечной точки. Спецификация явно задает, что случается, когда и что нужно сделать в случае условия возникновения ошибки. Самой большой трудностью при написании драйверов для таких устройств является обеспечить соответствие протокола USB в существующую поддержку для устройств хранения. CAM дает способы для этого ясным образом. ATAPI менее прост, так как исторически интерфейс IDE никогда не имел много различных представлений.
Поддержка дискет USB от Y-E Data опять же менее понятно, так как был разработан новый набор команд.
В подсистеме звука FreeBSD существует чёткое разделение между частью, поддерживающей общие звуковые возможности и аппаратно зависимой частью. Данная особенность делает более простым добавление поддержки новых устройств.
pcm(4) занимает центральное место в подсистеме звука. Его основными элементами являются:
Интерфейс системных вызовов (read, write, ioctls) к функциям оцифрованного звука и микшера. Командный набор ioctl совместим с интерфейсом OSS или Voxware, позволяя тем самым портирование мультимедиа приложений без дополнительной модификации.
Общий код обработки звуковых данных (преобразования форматов, виртуальные каналы).
Единый программный интерфейс к аппаратно-зависимым модулям звукового интерфейса.
Дополнительная поддержка нескольких общих аппаратных интерфейсов (ac97) или разделяемого аппаратно-специфичного кода (например: функции ISA DMA).
Поддержка отдельных звуковых карт осуществляется с помощью аппаратно-специфичных драйверов, обеспечивающих канальные и микшерные интерфейсы, включаемые в общий pcm код.
В этой главе термином pcm мы будем называть центральную, общую часть звукового драйвера, как противопоставление аппаратно-специфичным модулям.
Человек, решающий написать драйвер наверняка захочет использовать в качестве шаблона уже существующий код. Но, если звуковой код хорош и чист, он также в основном лишён комментариев. Этот документ - попытка рассмотрения базового интерфейса и попытка ответить на вопросы, возникшие при адаптировании существующего кода.
Для старта с рабочего примера, вы можете найти шаблон драйвера, оснащенного комментариями на http://people.FreeBSD.org/~cg/template.c
Весь исходный код, на сегодняшний момент (FreeBSD 4.4), содержится в каталоге /usr/src/sys/dev/sound/, за исключением публичных определений интерфейса ioctl, находящихся в /usr/src/sys/sys/soundcard.h
В подкаталоге pcm/ родительского каталога /usr/src/sys/dev/sound/ находится главный код, а в каталогах isa/ и pci/ содержатся драйвера для ISA и PCI карт.
Обнаружение и подключение звуковых драйверов во многом схоже с драйвером любого другого устройства. За дополнительной информацией вы можете обратиться к главам ISA или PCI данного руководства.
Но всё же, звуковые драйвера немного отличаются:
Они объявляют сами себя, как устройства класса pcm, с
частной структурой устройства struct snddev_info
:
static driver_t xxx_driver = { "pcm", xxx_methods, sizeof(struct snddev_info) }; DRIVER_MODULE(snd_xxxpci, pci, xxx_driver, pcm_devclass, 0, 0); MODULE_DEPEND(snd_xxxpci, snd_pcm, PCM_MINVER, PCM_PREFVER,PCM_MAXVER);
Большинство звуковых драйверов нуждаются в сохранении личной информации, касающейся их
устройства. Структура с личными данными обычно выделяется при вызове функции attach. Её
адрес передаётся pcm посредством вызовов pcm_register()
и mixer_init()
.
Позже pcm передаёт назад этот адрес, в качестве параметра в
вызовах к интерфейсам звукового драйвера.
Функция подключения звукового драйвера должна объявлять её микшерный или AC97
интерфейс pcm посредством вызова mixer_init()
. Для микшерного интерфейса это взамен вернёт вызов
xxxmixer_init()
.
Функция подключения звукового драйвера передаёт общие настройки каналов pcm посредством вызова pcm_register(dev,
sc, nplay, nrec)
, где sc
- адрес структуры данных
устройства, используемой в дальнейших вызовах от pcm, а nplay
и nrec
- количество каналов
проигрывания и записи.
Функция подключения звукового драйвера объявляет каждый из её каналов с помощью
вызовов pcm_addchan()
. Это установит занятость канала в pcm и вызовет взамен вызов xxxchannel_init()
.
Функция отключения должна вызывать pcm_unregister()
перед объявлением её ресурсов свободными.
Существует два метода работы с не PnP устройствами:
Использование метода device_identify()
(пример смотрите
в: sound/isa/es1888.c). device_identify()
пытается обнаружить оборудование, использующее
известные адреса, и если найдёт поддерживаемое устройство, то создаст новое pcm
устройство, которое затем будет передано процессу обнаружения/подключения.
Использование выборочной конфигурации ядра с соответствующими хинтами для pcm устройств (пример: sound/isa/mss.c).
pcm драйверы должны поддерживать device_suspend
, device_resume
и
device_shutdown
функции, для корректного функционирования
управления питанием и процесса выгрузки модуля.
Интерфейс между pcm и звуковыми драйверами определён в терминах объектов ядра.
Есть 2 основных интерфейса, которые обычно обеспечивает звуковой драйвер: канальный и, либо микшерный либо AC97.
Интерфейс AC97 довольно мало использует доступ к ресурсам оборудования (чтение/запись регистров). Данный интерфейс реализован в драйверах для карт с кодеком AC97. В этом случае фактический микшерный интерфейс обеспечивается разделяемым кодом AC97 в pcm.
Звуковые драйверы обычно имеют структуру с личными данными для описания их устройства и по одной структуре на каждый поддерживаемый канал проигрывания или записи данных.
Для всех функций канального интерфейса первый параметр - непрозрачный указатель.
Второй параметр это указатель на структуру с данными канала. Исключение: У channel_init()
это указатель на частную структуру устройства
(данная функция возвращает указатель на канал для дальнейшего использования в pcm).
Для передачи данных, pcm и звуковые драйвера используют
разделяемую область памяти, описанную в struct
snd_dbuf
.
struct snd_dbuf
принадлежит pcm, и звуковые драйверы получают нужные значения с помощью
вызовов функций (sndbuf_getxxx()
).
Область разделяемой памяти имеет размер, определяемый с помощью sndbuf_getsize()
и разделён на блоки фиксированного размера,
определённого в sndbuf_getblksz()
количества байт.
При проигрывании, общий механизм передачи данных примерно следующий (обратный механизму, используемому при записи):
В начале, pcm заполняет буфер, затем вызывает функцию
звукового драйвера xxxchannel_trigger()
с параметром PCMTRIG_START.
Затем звуковой драйвер многократно передаёт всю область памяти (sndbuf_getbuf()
, sndbuf_getsize()
)
устройству, с количеством байт, определённым в sndbuf_getblksz()
. Взамен это вызовет chn_intr()
pcm функцию для каждого
переданного блока (это обычно происходит во время прерывания).
chn_intr()
копирует новые данные в область, которая была
передана устройству (сейчас свободная) и вносит соответствующие изменения в структуру
snd_dbuf
.
xxxchannel_init()
вызывается для инициализации каждого
из каналов проигрывания или записи. Вызовы инициируются функцией подключения звукового
драйвера. (Подробнее в главе Обнаружение и
подключение).
static void * xxxchannel_init(kobj_t obj, void *data, struct snd_dbuf *b, struct pcm_channel *c, int dir){ struct xxx_info *sc = data; struct xxx_chinfo *ch; ... return ch;
}
b
- это адрес канальной struct
snd_dbuf
. Она должна быть инициализирована в функции посредством вызова sndbuf_alloc()
. Нормальный размер буфера для использования -
наименьшее кратное размера передаваемого блока данных для вашего устройства.c
- это указатель на структуру контроля pcm канала. Это не прозрачный объект. Функция должна хранить его
в локальной структуре канала, для дальнейшего использования в вызовах к pcm (например в: chn_intr(c)
).
dir
определяет для каких целей используется канал (PCMDIR_PLAY или PCMDIR_REC).
xxxchannel_setformat()
настраивает устройство на
конкретный канал определённого формата звука.
static int xxxchannel_setformat(kobj_t obj, void *data, u_int32_t format){ struct xxx_chinfo *ch = data; ... return 0; }
xxxchannel_setspeed()
устанавливает оборудование канала
на определённую шаблонную скорость и возвращает возможную корректирующую скорость.
static int xxxchannel_setspeed(kobj_t obj, void *data, u_int32_t speed) { struct xxx_chinfo *ch = data; ... return speed; }
xxxchannel_setblocksize()
устанавливает размер
передаваемого блока между pcm и звуковым драйвером, и между
звуковым драйвером и устройством. Обычно это будет количество переданных байт перед
прерыванием. Во время трансфера звуковой драйвер должен должен вызывать pcm функцию chn_intr()
каждый раз
при передаче блока данных такого размера.
Большинство звуковых драйверов только берут на заметку размер блока для использования во время передачи данных.
static int xxxchannel_setblocksize(kobj_t obj, void *data, u_int32_t blocksize) { struct xxx_chinfo *ch = data; ... return blocksize;}
xxxchannel_trigger()
вызывается pcm для контроля над трансферными операциями в драйвере.
static int xxxchannel_trigger(kobj_t obj, void *data, int go){ struct xxx_chinfo *ch = data; ... return 0; }
go
определяет действие для текущего вызова. Возможные
значения:PCMTRIG_START: драйвер должен начать передачу данных из или в
канальный буфер. Буфер и его размер могут быть получены через вызов sndbuf_getbuf()
и sndbuf_getsize()
.
PCMTRIG_EMLDMAWR / PCMTRIG_EMLDMARD: говорит драйверу, что входной или выходной буфер возможно был обновлён. Большинство драйверов игнорируют эти вызовы.
PCMTRIG_STOP / PCMTRIG_ABORT: драйвер должен остановить текущую передачу данных.
Замечание: Если драйвер использует ISA DMA,
sndbuf_isadma()
должна вызываться перед выполнением действий над устройством, она также позаботится о вещах со стороны DMA чипа.
xxxchannel_getptr()
возвращает текущее смещение в
передаваемом буфере. Обычно вызывается в chn_intr()
, и так
pcm узнаёт, где брать данные для новой передачи.
xxxchannel_free()
вызывается для освобождения ресурсов
канала. Например: должна вызываться, при выгрузке драйвера, если структуры данных канала
распределялись динамично или, если sndbuf_alloc()
не
использовалась для выделения памяти под буфер.
struct pcmchan_caps * xxxchannel_getcaps(kobj_t obj, void *data) { return &xxx_caps;}
channel_reset()
, channel_resetdone()
, и channel_notify()
предназначены для специальных целей и не должны
употребляться в драйвере без обсуждения с авторами (Cameron Grant <cg@FreeBSD.org>
).
channel_setdir()
is deprecated.
xxxmixer_init()
инициализирует оборудование и говорит
pcm какие микшерные устройства доступны для проигрывания и
записи
static int xxxmixer_init(struct snd_mixer *m) { struct xxx_info *sc = mix_getdevinfo(m); u_int32_t v; [Initialize hardware] [Set appropriate bits in v for play mixers]mix_setdevs(m, v); [Set appropriate bits in v for record mixers] mix_setrecdevs(m, v) return 0; }
Определения битов микшера могут быть найдены в soundcard.h (SOUND_MASK_XXX значения и SOUND_MIXER_XXX битовые сдвиги).
xxxmixer_set()
устанавливает уровень громкости для
одного микшерного устройства.
static int xxxmixer_set(struct snd_mixer *m, unsigned dev, unsigned left, unsigned right){ struct sc_info *sc = mix_getdevinfo(m); [set volume level] return left | (right << 8);
}
Допустимые значения уровней громкости лежат в пределах [0-100]. Равное нулю значение должно выключать звук устройства.
xxxmixer_setrecsrc()
устанавливает исходное записывающее
устройство.
static int xxxmixer_setrecsrc(struct snd_mixer *m, u_int32_t src){ struct xxx_info *sc = mix_getdevinfo(m); [look for non zero bit(s) in src, set up hardware] [update src to reflect actual action] return src;
}
xxxmixer_uninit()
должна проверить, что все звуки
выключены (mute), и, если возможно выключить оборудование микшера
xxxmixer_reinit()
должна удостовериться, что
оборудование микшера включено и все установки, неконтролируемые mixer_set()
или mixer_setrecsrc()
восстановлены.
Поддержка интерфейса AC97 осуществляется драйверами с кодеком AC97. Он поддерживает только три метода:
xxxac97_init()
возвращает количество найденных ac97
кодеков.
ac97_read()
и ac97_write()
читают или записывают данные определенного регистра.
Интерфейс AC97 используется кодом AC97 в pcm для выполнения операций более высокого уровня. За примером обращайтесь к sound/pci/maestro3.c или к другим файлам из каталога sound/pci/.
[1] |
Некоторые утилиты, такие как disklabel(8) могут хранить информацию в этой области, обычно во втором секторе. |
Этот, и другие документы, могут быть скачаны с ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.
По вопросам, связанным с FreeBSD, прочитайте документацию прежде чем писать в <questions@FreeBSD.org>.
По вопросам, связанным с этой документацией, пишите <doc@FreeBSD.org>.
По вопросам, связанным с русским переводом документации, пишите в рассылку <frdp@FreeBSD.org.ua>.
Информация по подписке на эту рассылку находится на сайте проекта перевода.