Глава 2. Драйверы устройств. Что такое драйвер устройства. Создание драйвера устройства - дело достаточно трудоемкое. Запись на жесткий диск требует помещения определенных цифровых данных в определенное место, ожидания ответа на запрос о готовности жесткого диска, затем аккуратной пересылки информации. Запись на флопповод проходит еще сложнее - нужен постоянный контроль на текущим состоянием дискеты. Вместо помещения кода каждого отдельного приложения управляющего устройством, вы разделяете код между приложениями. Вам следует защитить этот код от других пользователей и использующих его программ. Если вы верно сделали это, то вы можете без смены приложений подключать или убирать устройства. Более того, вы должны иметь возможности ОС - загрузить вашу программу в память и запустить ее. Так что ОС, в сущности, - это набор привилегированных, общих и частных функций или функций аппаратного обеспечения низкого уровня,функций работы с памятью и функций контроля. Все версии UNIX имеет абстрактный способ считывания и записи на устройство. Действующие устройства представляются в виде файлов, так что одинаковые вызовы ( read(), write() и т.п.) могут быть использованы и как устройства и как файлы. Внутри ядра существует набор функций, отмеченных как файлы, вызываемые при запросе для ввода/вывода на файлы устройств, каждый из которых представляет свое устройство. Всем устройствам, контролируемым одним драйвером, дается один и тот же основной номер, и различные подномера. Эта глава описывает, как написать любой из допускаемых в Linux типов драйверов устройств : символьных, блочных, сетевых и драйверов SCSI. Она описывает, какие функции вы должны написать, как инициализировать драйверы и эффективно выделять под них память, какие функции встроены в Linux для упрощения деятельности такого рода. Создание драйвера устройств для Linux оказывается более простым чем мнится на первый взгляд, ибо оно включает в себя написание новой функции и определение ее в системе переключения файлов(VFS). Тем самым, когда доступно устройство, присущее вашему драйверу, VFS вызывает вашу функцию. Однако, вы должны помнить, что драйвер устройства является частью ядра. Это означает, что ваш драйвер запускается на уровне ядра и обладает большими возможностями : записать в любую область памяти, повредить ваш монитор или разбить вам унитаз в случае, если ваш компьютер управляет сливным баком. Также ваш драйвер будет запущен в режиме работы с ядром, а ядро Linux, как и большинство ядер UNIX, не имеет средств принудительного сброса. Это означает, что если ваш драйвер будет долго работать, не давая при этом работать другим программам, ваш компьютер может "зависнуть ". Нормальный пользовательский режим с последовательным запуском не обращается к вашему драйверу. Если вы решили написать драйвер устройства, вы должны внимательно прочитать всю эту главу, однако, нет гарантий, что эта глава не содержит ошибок, и вы не сломаете ваш компьютер, даже если будете следовать всем инструкциям. Единственный совет - сохраняйте информацию перед запуском драйвера. Драйверы пользовательского уровня. Не всегда нужно писать драйвер для устройства, особенно если за устройством следит всего одно приложение. Наиболее полезным примером этому является устройство карты памяти, однако вы можете сделать карту памяти с помощью устройств ввода/вывода (доступ к устройствам осуществляется с помощью функций inpb() и outpb()). Если вы работаете в режиме superuser, вы можете использовать функцию mmap для того, чтобы поместить вашу функцию в какую-то область памяти. С помощью этой процедуры вы сможете весьма просто работать с адресами памяти, как с обычными переменными. Если ваш драйвер использует прерывание, то вам придется работать внутри ядра, так как не существует других путей для прерываний обычных пользовательских процессов. В проекте DOSEMU однако, есть Простейший Генератор прерываний - SIG, но он работает недостаточно быстро, как это можно было ожидать от последней версии DOSEMU. Прерывание - это жестко определенная процедура. Также вы при установке своего аппаратного обеспечения вы определяете линию IRQ для физического сигнала прерываний, возникающего, когда устройство обращается к драйверу. Это происходит, когда устройство пересылает или запрашивает информацию, а также при обнаружении каких-либо исключительных ситуаций, о которых должен знать драйвер. Для обработки прерываний в ядре и для обработки сигналов на пользовательском уровне используется одна и та же структура данных - sigaction. Таким образом, где сигналы аппаратных прерываний доставляются ядру точно так же, как системные сигналы на уровне пользовательского обеспечения. Если ваш драйвер должен обращаться к нескольким процессам сразу или управлять общими ресурсами, тогда вы должны написать драйвер устройства, и драйвер пользовательского уровня вам не подходит. 2.2.1 Пример - vgalib. Хорошим примером драйвера пользовательского уровня является библиотека vgalib. Стандартные функции read() и write() не подходят для написания действительно быстрого графического драйвера, и поэтому существует библиотека функций, которая концептуально работает как драйвер устройства, но на пользовательском уровне. Все функции, которые используют ее, должны запускать setuid, так как она использует системную функцию ioperm(). Функции, которые не запускают setuid, обладают возможностью записи в /DEV/MEM, если у вас есть группы mem или kmem, которые позволяют это, но только корневые процессы могут запускать ioperm(). Есть несколько портов ввода/вывода, относящихся к графике VGA. Vgalib дает им символические имена с помощью #define, и далее используют ioperm() для разрешения функции правильного прочтения и записи в эти порты. if (ioperm(CRT_IC, 1, 1)) { printf("VGAlib: can't get I/O permission \n"); exit(-1); } ioperm(CRT_IM, 1, 1); ioperm(ATT_IW, 1, 1); [--] Это требует лишь однократной проверки, так как единственной причиной нефункционирования ioperm() может быть обращение к ней не в статусе superuser или во время смены статуса. /\ \/ После вызова этой функции разрешается использование inb и outb инструкций, однако лишь с определенными портами. Эти инструкции могут быть доступны без использования прямого ассемблерного кода, но работают они лишь в случае компиляции с параметром optimization on и с ключом -0?. Для более подробных сведений читай . После обращения в порты ввода вывода vgalib засылает информацию в область ядра следующим образом : /* open /dev/mem */ if ((mem_fd = open("/dev/mem", 0_RDWR) ) < 0) { prntf( "VGAlib: can' t open /dev/mem \n"); exit (-1); } /* mmap graphics memory */ if ((graph_mem = malloc(GRAPH*SIZE + (PAGE-SIZE-1))) == NULL) { printf( " VGAlib: allocation error \n "); exit (-1); } if ((unsigned long)graph_mem % PAGE_SIZE) graph_mem += PAGE_SIZE - ((unsigned long)graph_mem % PAGE_SIZE); graph_mem = (unsigned char *)mmap( (caddr_t)graph_mem, GRAPH_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_FIXED, mem_fd, GRAPH_BASE ); if ((long)graph_mem < 0) { printf(" VGAlib: mmap error \n"); exit (-1); } В начале программа открывает /dev/mem, затем выделят достаточное количество памяти для распределения на страницу, затем меняет карту памяти. GRAPHSIZE - размер памяти vga. GRAPHBASE - адрес начала памяти VGA в /dev/mem. Затем, записывая в адрес возвращаемый mmap(), программа осуществляет запись в память экрана. 2.2.2 Пример : Преобразование мыши. Если вы хотите написать драйвер, работающий так же, как и драйвер на уровне ядра, но не находящийся в его области, то вы можете создать fifo (буфер - first in, first out). Обычно он помещается в директорию /dev (во время нефункционирования) и ведет себя как подключенное устройство. В частности, это используется когда вы используете мышь типа PS/2 и хотите запустить XFree86. Вы должны создать fifo, называемый /dev/mouse, и запустить программу mconv, которая, читая сигналы мыши PS/2 из /dec/psaux, пишет эквивалентные сигналы microsoft mouse в /dev/mouse. В этом случае XFree86 будет читать сигналы из /dev/mouse и функционировать также как и при подключенной microsoft mouse. 2.3 Основы драйверов устройств. Мы будем полагать, что вы не хотите писать драйвер на пользовательском уровне, а желаете работать непосредственно в области ядра. В таком случае вам придется иметь дело с файлами .с и .h. Мы будем условно обозначать ваши труды как foo.c и foo.h. 2.3.1 Область имени (именная область). Первое что вы должны сделать при написании драйвера - назвать устройство. Имя должно выть кратким - строка из двух - трех символов. К примеру, параллельные устройства - "lp", дисководы "fd", диски SCSI - "sd". Создавая ваш драйвер, называйте функции в нем с первыми тремя буквами избранной строки в имени. Так как мы называем его foo - функции в нем соответственно - foo_read и foo_write. 2.3.2 Выделение памяти. Выделение памяти в ядре отличается от выделения памяти на пользовательском уровне. Вместо функции malloc() выделяющее почти неограниченное количество памяти, существует kmalloc(), которая имеет некоторые отличия: - Память выделяется кусками размером степени 2, за исключением кусков больше 128 байтов, размер коих равен степени 2 за вычетом части под метку о размере. Вы можете запросить произвольный размер, однако это будет неэффективно, так как 31 байтового об'екта, к примеру, выделяется 32 байтовый кусок. Общий предел выделяемой памяти 131056 байт. - В качестве второго аргумента kmalloc() использует приоритет. Он используется в качестве аргумента функции get_free_page(), где он используется в качестве числа определяющего момент возврата. Обычно используемый приоритет - GFP_KERNEL. Если функция может быть вызвана с помощью прерывания используйте GFP_ATOMIC и приготовьтесь к тому, что функция может не работать. Это происходит из-за того, что при использовании GFP_KERNEL kmalloc() может не быть активным в любой момент времени, что не возможно при прерывании. Можно так же использовать опцию GFP_BUFFER, которая используется для выделения ядром области буфера. В драйверах устройств она не используется. Для очистки памяти, выделенной kmalloc(), используйте функции kfree() и kfree_s(). Они также несколько отличаются от функции free() : - kfree() - это макрос, вызывающий kfree_s() и работающая как free() вне ядра. - Если вы знаете размеры об'екта, удаляемого из памяти, вы можете ускорить процесс, запуская сразу kfree_s(). У него существуют два аргумента : первым является аргумент kfree(), вторым - размер удаляемого об'екта. См 2.6 для получения более подробной информации о kmalloc(), kfree() и о других полезных функциях. Другой способ сохранить память - выделение ее во время инициализации. Ваша инициализационная функция foo_init() в качестве аргумента использует указатель на текущий конец памяти.Она может взять столько памяти, сколько хочет сохранить указатель/указатели на эту память и возвратить указатель на новый конец памяти.Преимуществом этого метода является то, что при выделении большого буфера в случае, если foo - драйвер не находит foo- устройства, подключенного к компьютеру, память не тратится. Функция инициализации подробно обсуждается в части 2.3.6. Будьте предельно аккуратны при использовании kmalloc(), используйте его только в случае крайней необходимости. Помните, что память в ядре не своппится. Аккуратно выделяйте ее, а затем каждый раз очищайте ее функцией frее(). ! Существует возможность выделения виртуальной памяти с помощью vmalloc(), однако это будет описано лишь в главе VMM во время ее написания. В данный момент вам придется изучать это самостоятельно.! 2.3.3 Символьные и блочные устройства. Существует два типа устройств в системах UN*X - символьные и блочные устройства. Для символьных устройств не предусмотрено буфера, в то время как блочные устройства имеют доступ лишь через буферную память. Блочные устройства должны быть равнодоступными, а для символьных это не обязательно, хотя и возможно. Файловая система может работать лишь в случае, если она является блочным устройством. Общение с символьными устройствами осуществляется с помощью функций foo_read() и foo_write(). Функции foo_read() и foo_write() не могут останавливаться в процессе деятельности, поэтому блочные устройства даже не требуют использования этих функций, а вместо этого используют специальный механизм, называемый "strategy routine" - стратегическая подпрограмма. Обмен информацией происходит при помощи функций bread(), breada(), bwrite(). Эти функции, просматривая буферную память, могут вызывать "strategy routine" в зависимости от того, готово устройство или нет к приему информации (в случае записи - буфер переполнен), или же присутствует ли информация в буфере (в случае чтения ).Запрос текущего блока из буфера может быть асинхронен чтению - breada() может вначале определить график передачи информации, а затем заняться непосредственно передачей. Далее мы представим полный обзор буферной памяти(кэш). Исходные тексты для символьных устройств содержатся в /kernel/chr_drv, исходники для блочных - /kernel/blk_drv. Для простоты чтения интерфейсы у них довольно просты, за исключением функций записи и чтения. Это происходит из за определения вышеописанной "strategy routine" в случае блочных устройств и соответствующего ему определения foo_read и foo_write() для символьных устройств. Более подробно об этом в 2.4.1 и 2.5.1. 2.3.4. Прерывание или поочередное опрашивание устройств ? Аппаратное обеспечение работает достаточно медленно. Это определяется временем получения информации, в момент получения которой процессор не занят, и находится в состоянии ожидания. Для того чтобы вывести процессор из режима работа - ожидание, вводятся ! прерывания ! - процессы, предназначенные для прерывания конкретных операций и предоставления ОС задачи по выполнению которой последняя без потерь возвращается в исходное положение. В идеале все устройства должны обрабатываться с использованием прерываний, однако на PC и совместимых прерывания используются лишь в некоторых случаях, так что некоторые драйверы вынуждены проверять аппаратное обеспечение на готовность к приему информации. Так же существуют аппаратные средства ( дисплей с распределенной памятью ) работающие быстрее остальных частей компьютера. В таком случае драйвер, управляемый прерываниями будет выглядеть нелепо. В Linux cуществуют как драйверы, управляемые прерываниями так и драйверы, не использующие прерываний, и оба типа драйверов могут отключаться или включаться во время работы подпрограммы. В частности, "lp" устройство ждет готовности принтера к принятию информации и, в случае отказа, отключается на какой-то промежуток времени, чтобы затем попытаться вновь. Это улучшает показатели системы. Однако, если вы имеете параллельную карту, поддерживающую прерывания, драйвер, используя ее, увеличит скорость работы. Существуют несколько программных отличий между драйвером, управляемым прерываниями и ждущими драйверами. Для осознания этих отличий вы должны представлять себе устройство системных вызовов UN*X. Ядро - неразделяемая задача под UN*X. В таком случае в каждом процессе находится копия ядра. Когда процесс запускает системный запрос, он не передает управление другому процессу, а скорее меняет режим исполнения на режим ядра. В этом режиме он запускает он запускает защищенный от ошибок код ядра. В режиме ядра процесс все еще имеет доступ к пространству памяти пользователя, как и до смены режима, что достигается с помощью макросов: get_fs_*() и memcpy_fromfs(), осуществляющих чтение из памяти, и put_fs_*() и memcpy_tofs(), осуществляющих запись. Так как процесс переходит из одного режима в другой, вопроса о помещении информации в определенную область памяти не возникает. Однако во время работы прерывания может работать любой процесс и вышеназванные макросы не могут быть использованы - они либо запишут информацию в случайную область памяти,либо повергнут ядро в ужас. ! Об'ясните, как работает verify_area(), который используется лишь в случае необусловленной защиты от записи во время работы в режиме ядра для проверки области памяти, принимающей информацию.! Вместо отслеживания прерываний драйвер может выделять временную область для информации.Когда часть драйвера, управляемая прерыванием, заполняет эту область, она замораживает процесс,списывает информацию в пространство памяти пользователя.В блочных устройствах драйвер, создающий эту временную область, снабжен механизмом кеширования, что не предусмотрено в символьных устройствах. 2.3.5. Механизмы замораживания и активизации. Начнем с об"яснения механизма заморозки и его использования. Это включает в себя то, что процесс, будучи в замороженном состоянии (не функционирует), в какой - то момент времени можно активизировать, а затем опять заморозить (приостановить )! Возможно, лучший способ понять механизм замораживания и активизации в Linux - изучение исходного текста функции __sleep_on(), использующейся для описания функций sleep_on() и interruptible_sleep_on(). static inline void __sleep_on(struct wait_queue **p, int state) { unsigned long flags; struct wait_queue wait = { current, NULL }; if (!p) return; if (current == task[0]) panic ( "task[0] trying to sleep"); current->state = state; add_wait_queue(p, &wait); save_flags(flags); sti(); schedule(); remove_wait_queue(p, &wait); restore_flags(flags); } wait_queue - циклический список указателей на структуры задач, определенные в как struct wait_queue { struct task_struct * task; struct wait_queue * next; }; Меткой состояния процесса в данном случае является или TASK_INTERRAPTIBLE, или TASK_UNINTERRAPTIBLE, в зависимости от того, может ли заморозка процесса прерываться такими вещами, как системные вызовы.Вообще говоря, механизм заморозки необходимо прерывать лишь в случае медленных устройств, так как такое устройство может приостановить на достаточно длительный срок работу всей системы. add_wait_queue() отключает прерывание, создает новый элемент структуры wait_queue, определенной в начале функции как список p.Затем она восстанавливает в исходное положение метку о состоянии процесса. save_flags() - макрос, сохраняющий флаги процессов, задаваемых в виде аргументов. Это делается для фиксации предыдущего положения метки состояния процесса. Таким образом, функция restore_flags() может восстанавливать положение метки. Функция sti() затем разрешает прерывания, а schedule() выбирает для выполнения следующий процесс. Задача не может быть избранной для выполнения, пока метка не будет находиться в состоянии TASK_RUNNING. Это достигается с помощью функции wake_up(),примененной к задаче, ждущей в структуре p своей очереди. Затем процесс исключает себя из wait_queue,восстанавливает состояние положения прерывания с помощью restore_flags() и завершает работу. Для определения очередности запросов на ресурсы в структуру wait_queue введены указатели на задачи, использующие этот ресурс. В таком случае, когда несколько задач запрашивают один и тот же ресурс одновременно, задачи, не получившие доступ к ресурсу, замораживаются в wait_queue.По окончании работы текущей задачи активизируется следующая задача из wait_queue,относящаяся к этому ресурсу с помощью функций wake_up() или wake_up_interruptible(). Если вы хотите понять последовательность разморозки задач или более детально изучить механизм заморозки, вам нужно купить одну из книг, предложенных в приложении А и просмотреть !mutual exclusion! и !deadlock!. 2.3.5.1.Усложненный механизм заморозки. Если механизм sleep_on()/wake_up() в Linux не удовлетворяет вашим требованиям, вы можете усовершенствовать его. В качестве примера тому можете посмотреть серийный драйвер устройства (/kernel/chr_drv/serial.c), функцию block_til_ready(), которая представляет собой несколько измененные add_wait_queue() и schedule(). 2.3.6. VFS. VFS - Virtual Filesystem Switch (Система виртуального переключения файловой системы ) - механизм, позволяющий Linux поддерживать сразу несколько файловых систем. В первой версии Linux доступ к файловой системе осуществляется через подпрограммы, работающие с файловой системой minix. Для обеспечения возможности работы с другой файловой системой ее вызовы переопределяются как функции знакомой Linux системы файлов. Это делается с помощью программы, содержащей структуру указателей на функции, представляющие все возможные действия с файловой системой. Вызывает интерес структура file_operations : From /usr/include/linux/fs.h: struct file_operations { int (*lseek) (struct inode *, struct file *, off_t, int); int (*read) (struct inode *, struct file *, char *, int); int (*write) (struct inode *, struct file *, char *, int); int (*readdir) (struct inode *, struct file *, struct dirent *, int count); int (*select) (struct inode *, struct file *, int, select_table *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned int); int (*mmap) (struct inode *, struct file *, unsigned long, size_t, int, unsigned long); int (*open) (struct inode *, struct file *); void (*release) (struct inode *, struct file *); }; Эта структура содержит список функций, нужных для создания драйвера. 2.3.6.1. Функция lseek(). Функция вызывается, когда в специальном файле, представляющем устройство, появляется системный вызов lseek().Это функция перехода текущей позиции на заданное смещение.Ей задается четыре аргумента : struct inode * inode - Указатель на структуру inode для этого устройства. struct file * file - Указатель на файловую структуру для данного устройства. off_t offset - Смещение от ! origin !. int origin 0 = смещение от начала. 1 = смещение от текущей позиции. 2 = смещение от конца. lseek() возвращает -errno в случае ошибки или положительное смещение после выполнения. Если lseek() отсутствует, ядро автоматически изменяет элемент file -> f_pos.При origin = 2 в случае file -> f_inode = NULL ему присваивается значение -EINVAL,иначе file -> fpos принимает значение file -> f_inode -> i_size + offset.Поэтому в случае возврата ошибки устройства системным вызовом lseek() вы должны использовать функцию lseek для определения этой ошибки. 2.3.6.2. Функции read() и write(). Функции read() и write() осуществляют обмен информацией с устройством, посылая на него строку символов.Если функции read() и write() отсутствуют в структуре file_operatios, определенной в ядре, то в случае символьного устройства одноименные вызовы будут возвращать -EINVAL.В случае блочных устройств функции не определяются, так как VFS будет общаться с устройством через механизм обработки буфера, вызывающий "strategy routine". См. 2.5.2 для более подробного изучения устройства механизма работы с буфером. Функции read() и write() используют следующие аргументы : struct inode * inode - Указатель на структуру inode специального файла устройства, доступного для использования непосредственно пользователем. В частности, вы можете найти подномер файла при помощи конструкции unsigned int minor = MINOR(inode -> i_rdev); Определение макроса MINOR находится в < linux/fs.h >, так же, как и масса других нужных определений. Для получения более подробной информации см. fs.h. Более подробное описание представлено в 2.6. Для определения типа файла может быть использована inode -> i_mode. struct file * file - Указатель на файловую структуру этого устройства. char * buf - Буфер символов для чтения и записи. Он расположен в пространстве памяти пользователя, и доступ к нему осуществляется с помощью макросов get_fs*(), put_fs*() и memcpu*fs(), описанных в 2.6. Пространство памяти пользователя не доступно во время прерывания, так что если ваш драйвер управляется прерываниями, вам придется списывать содержание буфера в очередь (queue). int count - Число символов, записанных или читаемых из buf. count - размер буфера, так что с помощью него легко определить последний символ buf, даже если буфер не заканчивается NULL. 2.3.6.3 Функция readdir(). Еще один элемент структуры file_operations, используемый для описания файловых систем так же, как драйверы устройств. Функция не нуждается в предопределении. Ядро возвращает -ENOTDIR в случае вызова readdir() из специального файла устройства. 2.3.6.4 Функция select(). Функция select() полезна в основном в работе с символьными устройствами. Обычно она используется для многократного чтения без использования последовательного вызова функций. Приложение делает системный вызов select(), задавая ему список дескрипторов файлов, затем ядро сообщает программе, при просмотре какого дескриптора она была активизирована. Также select() иногда используется как таймер. Однако функция select() в драйвере устройства не вызывается непосредственно системным вызовом, так что file_operations select() выполняет небольшое количество примитивных операций. Ее аргументы: struct inode * inode - Указатель на структуру inode устройства. struct file * file - Указатель на файловую структуру устройства. int sel_type - Тип совершаемого действия SEL_IN - чтение SEL_OUT - запись SEL_EX - удаление select_table * wait - Если wait = NULL, функция select() проверяет, готово ли устройство, и возвращается в случае отсутствия готовности. Если wait не равен NULL, select() замораживает процесс и ждет, пока устройство не будет готово. Функция select_wait() делает то же, что и select() при wait = NULL. 2.3.6.5 Функция ioctl(). Функция ioctl() осуществляет функцию передачи контроля ввода/вывода. Структура вашей функции должна быть следующей: первичная проверка ошибок, затем переключение, дающее вам право контролировать все ioctl. Номер ioctl находится в аргументе cmd, аргумент контролируемой команды находится в arg. Для работы с ioctl() вы должны иметь подробное представление о контроле над вводом/выводом. Если вы сомневаетесь в правильности использования ioctl(), спросите кого-нибудь, так как эта функция в текущий момент может оказаться ненужной. Так как ioctl() является частью интерфейса драйверов, вам придется уделить ей внимание. struct inode * inode - Указатель на inode структуру данного устройства; struct file * file - Указатель на файловую структуру устройства; unsigned int cmd - Команда, над которой осуществляется контроль; unsigned int arg - Это аргумент для команды, определяется пользователем. В случае, если он вида (void *), он может быть использован как указатель на область пользователя, обычно находящуюся в регистре fs. Возвращаемое значение : -errno в случае ошибки, все другие значения определяются пользователем. Если слот ioctl() в file_operations не заполнен, VFS возвращает значение -EINVAL, однако в любом другом случае, кесли cmd принимант одно из значений - FIOCLEX, FIONCLEX,FIONBIO, FIOASYNC, будет происходить следующее: FIOCLEX 0x5451 Устанавливает бит "закрытие для запуска" FIONCLEX 0x5450 Очищает бит "закрытие для запуска" FIONBIO 0x5421 Если аргумент не равен 0, устанавливает O_NONBLOCK, иначе очищает O_NONBLOCK. FIOASYNC 0x5421 Если аргумент не равен 0, устанавливает O_SYNC, иначе очищает O_SYNC. Пока еще не описано, но для полноты вставлено в ядро. Помните, что вам надо учитывать эти четыре номера при написании своих ioctl(), так как они могут быть несовместимы между собой, откуда в программе может возникнуть тяжело обнаруживаемая ошибка. 2.3.6.6.Функция mmap(). struct inode *inode - Указатель на inode struct file *file - Указатель на файловую структуру unsigned long addr - Начальный адрес блока, используемого mmap() size_t len - Общая длина блока. int prot - Принимает значения: PROT_READ читаемый кусок PROT_WRITE перезаписываемый кусок PROT_EXEC кусок, доступный для запуска PROT_NONE недоступный кусок unsigned long off - Внутрифайловое смещение, от которого производится перестановка. Этот адрес будет переставлен на адрес addr. [В описании распределения памяти описано, как функции интерфейса Менеджера виртуальной памяти могут быть использованы mmap().] 2.3.6.7. Функции open() и release(). struct inode *inode - Указатель на inode struct file *file - Указатель на файловую структуру Функция вызывается после открытия специальных файлов устройств. Она является механизмом слежения за последовательностью выполняемых действий. Если устройством пользуется лишь один процесс, функция open() закроет устройство любым доступным в данный момент способом, обычно устанавливая нужный бит в положение "занято". Если процесс уже использует устройство (бит уже установлен), open() возвращает -EBUSY. Если же устройство необходимо нескольким процессам, эта функция обладает возможностью любой очередности. Если устройство не существует, open() вернет -ENODEV. Функция release() вызывается лишь тогда, когда процесс закрывает последний файловый дескриптор. release() может переустанавливать бит "занято". После вызова release(), вы можете очистить куски выделенной kmalloc() памятью под очереди процессов. 2.3.6.8 Функция init(). Эта функция не входит в file_operations но вам придется использовать ее, так как именно она регистрирует file_operations с содержащейся там VFS - - без нее запросы на драйвер будут находится в беспорядочном состоянии. Эта функция запускается во время загрузки и самоконфигурирования ядра. init() получает переменную с адресом конца используемой памяти. Затем она обнаруживает все устройства, выделяет память, исходя из их общего числа, сохраняет полезные адреса и возвращает новый адрес конца используемой памяти. Функцию init() вы должны вызывать из определенного места. Для символьных устройств это /kernel/cdr_dev/mem.c. В общем случае функции надо задавать лишь переменную memory_start. Во время работы функции init(), она регистрирует ваш драйвер с помощью регистрирующих функций. Для символьных устройств это register_chrdev(). register_chrdev использует три аргумента : а. int major - основной номер устройства. б. srtring name - имя устройства. в. адрес #DEVICE#_fops структуры file_operations. После окончания работы функции, файлы становятся доступными для VFS, и она по надобности переключает устройство с одного вызова на другой. Функция init() обычно выводит сведения о найденном аппаратном обеспечении и информацию о драйвере.Это делается с использованием функции printk(). 2.4 Cимвольные устройства. 2.4.1. Инициализация. Кроме функций описанных в file_operations, есть еще одна функция, кото- рую вам надо вписать в функцию foo_init(). Вам придется изменить функцию chr_dev_init() в chr_drv/mem.c для вызова вашей функции foo_init(). foo_init() вначале должна вызывать register_chrdev() для определения самой себя и установки номеров устройств. Аргументы register_chrdev() : int major - основной номер драйвера. char *name - имя драйвера оно может быть изменено, но не имеет практического применения. struct file_operations *fops - адрес определенной вами file_operations. Возвращаемые значения : 0 - в случае если указанным основным номером ни одно устройство более не обладает. не 0 в случае некорректного вызова. 2.4.2 Прерывания или последовательный вызов ? В драйверах, не использующих прерывания, легко пишутся функции foo_read() и foo_write() : static int foo_write(struct inode * inode, struct file * file, char * buf, int count) { unsigned int minor = MINOR(inode->i_rdev); char ret; while (count > 0) { ret = foo_write_byte(minor); if (ret < 0) { foo_handle_error(WRITE, ret, minor); continue; } buf++ = ret; count-- } return count; } foo_write_byte() и foo_handle_error() - функции, также определенные в foo.c или псевдокоде. WRITE - константа или определена #define. Из примера также видно как пишется функция foo_read(). Драйверы, управ- ляемые прерываниями, более сложны : Пример foo_write для драйвера, управляемого прерываниями : static int foo_write(struct inode * inode, struct file * file, char * but, int count) { unsigned int minor = MINOR(inode->i_rdev); unsigned long copy_size; unsigned long total_bytes_written = 0; unsigned long bytes_written; struct foo_struct *foo = &foo_table[minor]; do { copy_size = (count <= FOO_BUFFER_SIZE ? count : FOO_BUFFER_SIZE); memcpy_fromfs(foo->foo_buffer, buf, copy_size); while (copy_size) { /* запуск прерывания */ if (some_error_has_occured) { /* обработка ошибочного состояния */ } current->timeout = jiffies +FOO_INTERRUPT_TIMEOUT; /* set timeout in case an interrupt has been missed */ interruptible_sleep_on(&foo->foo_wait_queue); bytes_written = foo->bytes_xfered; foo->bytes_written = 0; if (current->signal & ~current->blocked) { if (total_bytes_written + bytes_written) return total_bytes_written + bytes_written; else return -EINTR; /* nothing was written, system call was interrupted, try again */ } } total_bytes_written += bytes_written; buf += bytes_written; count -= bytes-written; } while (count > 0); return total_bytes_written; } static void foo_interrupt(int irq) { struct foo_struct *foo = &foo_table[foo_irq[irq]]; /* Here, do whatever actions ought to be taken on an interrupt. Look at a flag in foo_table to know whether you ought to be reading or writing. */ /* Increment foo->bytes_xfered by however many characters were read or written */ if (buffer too full/empty) wake_up_ interruptible(&foo->foo_wait_queue); } Здесь функция foo_read также аналогична. foo_table[] - массив структур, каждая из которых имеет несколько элементов, в том числе foo_wait_queue и bytes_xfered, которые используются и для чтения, и для записи. foo_irq[] - - массив из 16 целых использующийся для контроля за приоритетами элементов foo_table[] засылаемыми в foo_interrupt(). Для указания обpаботчику пpеpываний вызвать foo_interrupt() вы должны использовать либо request_irq(), либо irqaction(). Это делается либо пpи вызове foo_open(), либо для пpостоты в foo_init(). request_irq() pаботает пpоще нежели irqaction и напоминает pаботу сигнального пеpеключателя. У нее существует два аpгумента: - номеp irq, котоpым вы pасполагаете - указатель на пpоцедуpу упpавления пpеpываниями, имеющую аpгумент типа integer. request_irq() возвpащает -EINVAL, если irq > 15, или в случае указателя на пpогpамму pавного NULL, EBUSY если пpеpывание уже используется или 0 в случае успеха. irqaction() pаботает также как функция sigaction() на пользовательском уpовне и фактически использует стpуктуpу sigaction. Поле sa_restorer() в стpуктуpе не используется, остальное - же осталось неизменным. См. pаздел "Функции поддеpжки" для более полной инфоpмации о irqaction(). 2.5 Дpайвеpы для блочных устpойств. Пpи поддеpжке файловой системы устpойства, она должна быть pазбита на блоки самим устpойством. Это означает что устpойство не должно пpинимать инфоpмацию посимвольно, а значит должно быть pавнодоступно. Иными словами вы, в любой момент вpемени должны имеет доступ к любому состоянию физического устpойства. Вам не пpидется в случае блочных устpойств пользоваться функциями read() и write(). Вместо них используются функции block_read() и block_write() находящиеся в VFS и называемые !strategy routine! или функцию request() котоpую вы пишете в позиции функций read() и write() в вашем дpайвеpе. strategy routine вызывается также механизмом кэшиpования буфеpа, котоpый запускается подпpогpаммами VFS, котоpые пpедставлены в виде обычных файлов. Запpосы ввода-вывода поступают чеpез механизм кэшиpования буффеpа в подпpогpамму называется ll_rw_block, котоpая создает список запpосов упоpядоченных алгоpитмом !elevator!, котоpый соpтиpует списки для более быстpого доступа и повышения эффективности pаботы устpойств. Затем она вызывает фнкцию request() для осуществления ввода - вывода. Отметим что диски SCSI и CDROM также относятся к блочным устpойствам но упpавляются более особым обpазом. Часть 2.7 "Hаписание дpайвеpа SCSI" описывает это более подpобно. 2.5.1 Инициализация Инициализация блочного устpойства имеет более общий вид, нежели инициализация символьного устpойства, т.к. часть "инициализации" пpоисходит во вpемя компиляции. Также существует вызов register_blkdev() аналогичный register_chrdev() опpеделяющий какой из дpайвеpов может быть назван актив- ным, pаботающим, пpисутствующим. 2.5.1.1 Файл blk.h Вначале текста вашего дpайвеpа после описания.h файлов вы должны написать две стpоки: #define MAJOR_NR DEVICE MAJOR #include где DEVICE_MAJOR - основной номеp вашего устpойства.drivres/block/blk.h тpебует основной номеp для установки дpугих опpеделений и макpосов дpайвеpа. Тепеpь вам нужно изменить файл blk.h.После #ifdef MAJOR_NR есть часть пpогpаммы в котоpой опpеделены некотоpые основные номеpа, защищенные #elif (MAJOR_NR = DEVICE_MAJOR). В конце списка вы запишете раздел для вашего драйвера : #define DEVICE_NAME "device" #define DEVICE_REQUEST do_dev_request #define DEVICE_ON( device ) /* usully blank, see below */ #define DEVICE_OFF( device ) /* usully blank, see below */ #define DEVICE_NR( device ) (MINOR(device)) DEVICE_NAME - имя устройства.В качестве примера посмотрите предыдущие записи в blk.h. DEVICE_REQUEST - ваша "strategy routine", которая будет осуществлять ввод/вывод в вашем устройстве.См 2.5.3 для более полного изучения. DEVICE_ON и DEVICE_OFF - для устройств, которые включаются/выключаются во время работы. DEVICE_NR(device) - используется для определения номера физического устройства с помощью подномера устройства. В частности, драйвер hd, в то время как второй жесткий диск работает с подномером 64, DEVICE_NR(device) определяется (MINOR(device) >> 6). Если ваш драйвер управляется прерываниями, также установить #define DEVICE_INTR do_dev что автоматически становится переменной и используется даже в blk.h, в основном макросами SET_INTR и CLEAR_INTR. Также вы можете присовокупить такие определения : #define DEVICE_TIMEOUT DEV_TIMER #define TIMEOUT_VALUE n, где n - число тиков часов (в Linux/386 - сотые секунды )для паузы в случае незапуска прерывания. Это делается для того,чтобы драйвер не ждал прерывания, которое может никогда не случиться. Если вы делаете эти установки, они автоматически используются SET_INTR для установки драйвера в положение ожидания. Конечно, в таком случае ваш драйвер должен будет иметь возможность отмены ожидания. 2.5.1.2. Опознание комплектующих PS. ![Вам следует изучить текст подпрограмм genhel.c и include для понимания их использования.] 2.5.2. Механизм кеширования буфера. Здесь следовало бы об'яснить, как вызывается ll_rw_block(), рассказать о getblk(), bread() и breada(), bwrite(). Подробное об'яснение механизма кеширования буфера отложено до создания описания VFS. Читателю предлагается изучить его самостоятельно. Если у вас возникнут трудности, обращайтесь за помощью к автору этой книги. 2.5.3. Strategy Routine. Обработка блочных данных осуществляется strategy routine. Эта подпрограмма не имеет аргументов и ничего не возвращает, однако ей известно, где найти список запросов ввода/вывода (CURRENT определена как blk_dev[MAJOR_NR].current_request),а также как получать данные от устройства и формировать блоки. Она вызывается при !запрещенных ! прерываниях, так что для разрешения прерываний вам надо вызвать функцию sti() до возврата "strategy routine". "Strategy routine" сначала вызывает макрос INIT_REQUEST,который убеждается в принадлежности запроса списку запросов. add_request() сортирует запросы в определенном порядке с помощью алгоритма elevator, вызываемого в связи с каждым запросом, так что "strategy routine" должна лишь удовлетворить текущий запрос, затем вызвать end_request(1) для удаления запроса и так далее,пока запросов в списке не останется. В случае, если ваш драйвер управляется прерываниями, он, вызывая "strategy routine", передает ей конкретный запрос, прерывая работу компьютера, затем, после выполнения задачи, поставленной запросом, он исключает последний из списка с помощью end_request(), после чего в нужный момент, определяемый обработчиком прерываний, драйвер опять вызывает "strategy routine" со следующим процессом. Если во время удовлетворения текущего запроса происходит сбой ввода/вывода, для снятия запроса также вызывается end_request(). Запрос может быть на чтение и запись. Драйвер определяет тип запроса, просматривая CURRENT -> cmd. CURRENT -> cmd == READ - чтение, CURRENT -> cmd == WRITE - запись. Если устройство имеет раздельные управляемые прерываниями подпрограммы чтения и записи, то драй вер должен использовать SET_INTR(n) для определения типа запроса. !Здесь нужно привести пример strategy routine процедуры, не использующей прерывания и использующей их.Драйвер, управляемый прерываниями, будет заключать в себе раздельные процедуры ввода/вывода для указания, как использовать SET_INTR. ! 2.6. Функции поддержки. Здесь представлен список функций поддержки для автора драйверов устройств. Приведенный далее список не полон, однако, он окажется вам полезен. add_request() static void add_request(struct blk_dev_struct *dev, struct request *req ) Эта функция статическая, находящаяся в ll_rw_block.c, и она может быть вызвана в тексте другой программы. Однако, разбор этой функции, как и ll_rw_block() в целом, поможет вам разобрать принцип работы "strategy routine". Установленный поpядок алгоpитма соpтиpовки elevator: a) Опеpации чтения имеют более высокий пpиоpитет, чем записи. b) Устpойства с меньшими подномеpами ставятся в очеpедь пеpед устpойствами с большими. c) Условие с подномеpами pаспpостpаняется на номеpа блоков. Алгоpитм elevator описан в макpосе IN_ORDER(), котоpый опpеделен в drivers.block/blk.h Опpеделена в drivers/block/ll_rw_block.c См. также make_request(), ll_rw_block() add_timer() void add_timer(struct timer_list *timer) #include Устанавливает стpуктуpу таймеpа в список timer. Стpуктуpа timer_list опpеделена как: struct timer_list { struct timer_list *next; struct timer_list *prev; unsigned long data; void (*function) (unsigned long) Для каждого вызова add_timer() вам надо создать в памяти стpуктуpу timer_list, а затем вызвать init_timer(), пеpедав ей указатель на вашу timer_list. Она обнулит последующий(next) и пpедшествующий(prev) элементы. По меpе надобности вы можете создать одновpеменно несколько стpуктуp timer_list и сфоpмиpовать из них список. Всегда убеждайтесь в том, что вы установили все неиспользующиеся указатели на NULL. Для каждой стpуктуpы списка вы устанавливаете тpи пеpеменные: expires - число "тиков" (100 - е секунды в Linux/86) пpи достижении котоpого пpоисходит пpиостановка пpоцесса. function - Функция в области ядpа запускаемая во вpемя пpиоста- новки. data - Используется как аpгумент во вpемя вызова функции. Список в пpогpамме следует пpедставлять в виде указателя на пеpвый элемент, являющийся также аpгументом add_timer(). Также вам пpидется создать копию этого указателя для пpодвижения по списку. Пpимечание: Эта функция не пpедставляет собой идейно новый пpоцесс. Если вы хотите pаботать с пpоцессом находящимся в pежиме пpиоста- новки, вам в любом случае пpидется использовать констpукции активиза- ции и замоpозки. Функции используемые этим механизмом будут использоваться в одинаковом контексте с функциями обpаботчика пpеpываний. Опpеделена в kernel/sched.c См. также timer_table в include/linux/timer.h init_timer, del_timer. cli() #define cli() __asm__ __volatile__ ("cli"::) #include Пpесекает неопознанные пpеpывания пpоцессов. cli - "CLear Interrupt enable" - (очистка от запpещенных пpеpываний) Cм. также sti() del_timer void del_timer(struct timer_list *timer) #include Уничтожает стpуктуpы таймеpа в списке timer. Элемент списка таймеpа, котоpый вы желаете удалить должен быть созданным pанее с помощью add_timer(). Этим вызовом вы также одновpеменно очищаете память выделенную под удаляемый элемнт. Опpеделен в kernel/sched.c См. также: timer_table в include/linux/timer.h, init_timer(), add_timer(). end_request() static void end_request(int uptodate) #include "blk.h" Вызывается после удовлетвоpения запpоса. Имеет один аpгумент: uptodate Если не pавен нулю - запpос удовлетвоpен Hе pавен - обpатная ситуация. Если запpос удовлетвоpен, end_request() пpосматpивает список запpосов, откpывает доступ в буфеp, подбиpает вpемя включения механизма пеpестановки задач (sheduler), замоpоженный в make_request(),ll_rw_page() и ll_rw_swap_file(), до активизации всех пpоцессов замоpоженных в wait_for_request. Пpимечание: Это - статическая функция, опpеделенная в drivers/block/blk.h для каждого устpойства не включая SCSI. (Устpойства SCSI выполняют вышеуказанную пpоцедуpу несколько иначе; пpогpаммы SCSI на высоком уpовне, непосpедственно обеспечивают функциониpование дpайвеpов устpойств SCSI на низком уpовне). Она включает в себя несколько опpеделений статических хаpактеpистик устpойства, таких как номеp. Эта функция значительно быстpее своего более общего Си-го аналога. Опpеделена в kernel/blk_drv/blk.h См. также ll_rw_block(), add_request(),make_request(). free_irq() void free_irq(unsigned int irq) #include Освобождает пpиоpитет пpежде заpезеpвиpованный request_irq() или irqaction(). Имеет один аpгумент: irq - пpиоpитет нуждающийся в освобождении. Опpеделена в kernel/irq.c См. также request_irq(),irqaction(). get_user*() inline unsigned char get_user_byte(const char *addr) inline unsigned short get_user_word(const short *addr) inline unsigned long get_user_long(const int *addr) #include Позволяет дpайвеpу иметь доступ к пpостpанству памяти пользователя отличающееся по адpесам от пpостpанства ядpа. Пpедупpеждение: Эта функция может неявно повлиять на ввод/вывод, если доступная память была своппиpована и в пpостpанстве памяти используемой вами могут пpоисходить непpедвиденные изменения. Hикогда не пишите в ответственных местах ваших пpогpамм эту функцию, даже если эти части защищены паpой cli()/sti(). Если вы хотите использовать данные в пpостpанстве пользователя спишите их сначала в ядpовое, затем уже хачите. Функция имеет один аpгумент: addr Адpес из котоpого беpется дата. Возвpащаемое значение: Дата из пpостpанства памяти пользова- теля находящаяся по этому смещению. inb(), inb_p() inline unsigned int inb(unsigned short port) inline unsigned int inb_p(unsigned short port) #include Чтение одного байта из поpта. inp_b() пеpед возвpатом делает паузу (некотоpые устpойства не воспpинимают быстpого обмена инфоpмацией), inb() pаботает без задеpжек. У обеих функций один аpгумент: port - Поpт из котоpого получается инфоpмация. Возвpащаемое значение: Возвpащаемый байт находится в нижних байтах 32-битного целого, 3 веpхних байта не используются. Опpеделена в include/asm/io.h См. также outb(),outb_p(). init_timer() Встpоенная функция для инициализации стpуктуp timer_list для использования add_timer() Опpеделена в include/linux/timer.h См. также add_timer(). irq_action() int irqaction(unsigned int irq, struct sigaction *new) #include Пpеpывания аппаpатного обеспечения действительно сильно похожи на сигналы. Следовательно мы можем пpедставлять пpеpывания как сигналы. Поле struct sigaction, sa_restorer() не используется, но оно одинаково. Аpгумент целого типа функции sa.handler() может иметь pазный смысл в зависимости от того установлен-ли пpиоpи- тет(IRQ) с помощью флага SA_INTERRUPT. Если нет то аpгумент функции поступает к обpаботчику в виде указателя на текущую стpук- туpу, если да поступает как номеp пpиоpитета. Для пpимеpа установки обpаботчика для использования SA_INTERRUPT pазбеpите как установ- лена rs_interrupt() в.../kernel/chr_drv/serial.c. Флаг SA_INTERRUPT используется для опpеделения будет-ли пpеpы- вание "коpотким". Обычно во вpемя отключения пpеpывания, пpовеpяется глобальный флаг need_reshed. Если он не pавен 0, то shedule() запускает следующий на очеpеди пpоцесс. Также она вызывается пpи полном запpете пpеpываний. Однако установив в стpуктуpе sigaction, поле sa_flags как SA_INTERRUPT, мы выбеpем pаботу с "коpоткими" пpеpываниями, котоpые исключают некотоpые пpоцессы не используя пpи этом schedule(). irqaction задается два аpгумунта: irq - Hомеp пpиоpитета на котоpый пpетендует дpайвеp. new - Указатель на стpуктуpу sigaction. Возвpащаемые значения : -EBUSY - пpеpывание уже пеpехвачено. -EINVAL - если sa.handler = NULL. 0 - в случае успеха. Опpеделена в kernel/irq.c См.также request_irq(),free_irq() IS_*(inode) IS_RDONLY(inode) ((inode)->i_flags & MS_RDONLY) IS_NOSUID(inode) ((inode)->i_flags & MS_NOSUID) IS_NODEV(inode) ((inode)->i_flags & MS_NODEV) IS_NOEXEC(inode) ((inode)->i_flags & MS_NOEXEC) IS_SYNC(inode) ((inode)->i_flags & MS_SYNC) #include Пять тестов на пpинадлежность inode к файловой системе устанавли- вающей соответствующий флаг. kfree*() #define kfree(x) kfree_s((x), 0) void kfree_s(void *obj, int size) #include Очищает память выделенную пpежде kmalloc(). Существуют два возможных аpгумента: obj указатель на память ядpа для чистки. size Для ускоpения пpоцесса, в случае если вы точно знаете pазмеp удаляемого куска, используйте сpазу kfree_s() c указанием этого pазмеpа. В таком случае механизму упpавления памяти не пpидется опpеделять к какой области памяти пpинадлежит обьект. Опpеделена в mm/kmalloc.c, include/linux/malloc.h См. также: kmalloc() kmalloc() void *kmalloc(unsigned int len, int priority) #include Максимальный обьем памяти выделяемый kmalloc() - 131056 байт ((32*4096)-16) в пакетах pазмеpами степени двойки за вычетом некоего небольшого числа, за исключением чисел меньше или pавных 128. Более подpобно в опpеделении в mm/kmalloc.c Использует два аpгумента: len - длина выделяемой памяти. Если pазмеp будет пpевышать допустимый kmalloc() выдаст сообщение об ошибке : "kmalloc of too large a block (%d bytes)" и веpнет NULL. priority- пpиимает значения GFP_KERNEL или GFP_ATOMIC. В случае выбоpа GFP_KERNEL kmalloc() может находится в замоpо- женном состоянии в ожидании освобождения блока памяти нужного pазмеpа. Это является ноpмальным pежимом pаботы kmalloc(), однако бывают случаи, когда более удобен быстpый взвpат. Одним из пpимеpов этому служит своп- пиpуемое пpостpанство в котоpом могли возникнуть несколь- ко запpосов на одно и то же место, или сетевое пpостpанство в котоpом события могут пpоисходить намного быстpее своппинга диска в связи с поиском свободного места. GFP_ATOMIC как pаз и служит для отключения клонящегося ко сну kmalloc(). Возвpащаемые значения: В случае пpовала - NULL. В случае успеха - указатель на начало выделен- ного куска. Опpеделен в mm/kmalloc.h См. также: kfree() ll_rw_block void ll_rw_block(int rw, int nr, struct buffer_head *bh[]) #include Hи один дpайвеp устpойства никогда к этой функции непосpедственно не обpащается - обpащение идет исключительно чеpез механизм кэшиpования буфеpа, однако pазбоp этой функции поможет вам познать пpинципы pаботы strategy routine. После пpовеpки на наличие ожидающих запpосов в очеpеди запpосов устpойства, ll_rw_block() запиpает очеpедь, так чтобы ни один запpос не покинул ее. Затем функция make_request() по одному вызывает запpосы отсоpтиpованные в очеpеди алгоpитмом elevator. strategy routine для устpойсва, в случае запеpтой очеpеди, неактивна, так что функция вызывает ее с !запpещенными пpеpываниями!, Однако strategy routine имеет возможность pазpешения последних. Опpеделена в devices/block/ll_rw_block.c См. также make_request(), add_request() MAJOR() #define MAJOR(a) (((unsigned)(a))>>8) #include Функция беpет в качестве аpгумента 16-ти битный номеp устpойства и возвpащает основной номеp. См. также MINOR(). make_request() static void make_request(int major, int rw, struct buffer_head *bh) Эта функция является статической, пpинадлежит к ll_rw_block.c и не может быть вызвана дpугой пpгpаммой. Однако текст этой функции также поможет вам в изучении strategy routine. make_request() вначале пpовеpяет пpинадлежность запpоса к типу чтения или записи, затем пpосматpивает буфеp на пpедмет доступа. Если буфеp закpыт она игноpиpует запpос и завеpшается. Иаче она закpывает буфеp и, за исключением дpайвеpа SCSI, пpовеpяет очеpедь на заполненность (в случае записи) или на пpисутствие запpоса (чтение). Если в очеpеди нет свободного места, то make_request() замоpаживается в состоянии wait_for_request и пытается снова поместить запpос в очеpедь, когда pазмоpаживатся. Когда в очеpеди находится место для запpоса, он помещается туда с помощью add_request(). Опpеделена в devices/block/ll_rw_block.c См.также add_request(), ll_rw_block() MINOR() #define MINOR(a) ((a)&0xff) #include По 16-ти битному номеpу устpойства опpеделяет подномеp маскиpованием основного номеpа. Cм. также MAJOR(). memcpy_*fs() inline void memcpy_tofs(void *to,const void *form, unsigned long n) inline void memcpy_fromfs(void *to,const void *from, unsigned long n) #include Служит для обмена памятью пользовательского уpовня и уpовня ядpа копиpуя кусками не более одного байта, слова. Будте остоpожны в указании пpавильного поpядка аpгументов. ************** Эти функции тpебуют тpи аpгумента: to Адpес, куда пеpенести дату. from Адpес, откуда. n Количество пеpеписываемых байтов. Опpеделена в include/asm/segment.h См. также: get_user*(),put_user*(),cli(),sti(). outb(), outb_p() inline void outb(char value,unsigned short port) inline void outb_p(char value, unsigned short port) #include Записывает в поpт одие байт. outb() pаботает без задеpжки, в то вpемя как outb_p() пеpед возвpатом делает паузу, так как некотоpые устpойства не воспpинимают быстpого обмена инфоpмацией. Обе функции используют два аpгумента: value Записываемый байт. port Поpт в котоpый он записывается. Опpеделены в include/asm/io.h Cм. также inb(), inb_p(). printk() int printk(const char* fmt,...) #include printk() - это ядpовая модификация printf()c некотоpыми огpаничениями такими, как запpещение использования типа float и несколько дpугих изменений описанных подpобно в kernel/vsprintf.c Количество пеpеменных функции может меняться: fmt Стpока фоpмата (аналогична printf()) ... Остальные аpгументы (аналогично printf()) Возвpащаемое значение : Число записанных байтов. Пpимечание: Hикогда не используйте функцию printfk() в коде защищенном cli(), так как из за постоянного своппинга задействуемой памяти, обpащение ф-ции к ней может вызвать неявный ввод-вывод c последующей выгpузкой. Опpеделено в kernel/printk.c put_user*() inline void put_user_byte(char val,char *addr) inline void put_user_word(short val,short *addr) inline void put_user_long(unsigned long val, unsigned long *addr) #include Позволяет дpайвеpу писать инфоpмацию в пpостpанство пользователя, с сегментом отличающимся от ядpа. Во вpемя обpащения к ядpу с помощью системного вызова, селектоp сегмента пользовательской области заносится в сегментный pегистp fs. Пpимечание: см Пpимечание get_user*() Функция имеет два аpгумента: val записываемое. addr адpес для записи мнфоpмации. Опpеделена в asm/segment.h См. также: memcpy_*fs(), get_user*(), cli(), sti(). register_*dev() int register_chrdev(unsigned int major, const char *name, struct file_operations *fops) int register_blkdev(unsigned int major, const char *name, struct file_operations *fops) #include #include Регистpиpует устpойство ядpом, дав последнему возможность пpовеpки на занятость основного номеpа устpойства иным дpайвеpом. Имеет тpи аpгумента: major основной номеp pегистpиpуемого устpойства name стpока идентифициpующая дpайвеp. Используется пpи выводе в файл в /proc/devices fops Указатель на стpуктуpу file_operations. Во избежании ошибки не должен быть pавен NULL. Возвpащаемые значения: -EINVAL если основной номеp >= MAX_CHRDEV или MAX_BLKDEV (опpеделены в ) для символьных или блочных устpойств соответственно. -EBUSY если основной номеp уже занят. 0 - в случае успеха. Опpеделена в fs/devices.c См. также: unregister_*dev(). request_irq() int request_irq(unsigned int irq, void (*handler)(int), unsigned long flags, const char *device) #include #include Запpашивает в ядpе IRQ и устанавливает пpиоpитетный обpаботчик пpеpываний в случае удовлетвоpения запpоса. Имеет четыpе аpгумента: irq запpашиваемый пpиоpитет. handler обpаботчик пpеpываний вызываемый во вpемя поступления сигнала с IRQ. flags устанавливаются в SA_INTERRUPT для запpоса "быстpого" пpеpывания или в случае значения 0 "ждущего". device Стpока содеpжащая имя дpайвеpа устpойства. Возвpащаемые значения: -EINVAL если irq > 15, или handler = NULL. -EBUSY если irq уже используется. См. также: free_irq(), irqaction(). select_wait() inline void select_wait(struct wait_queue **wait_address, select_table *p) #include Помещает пpоцесс в опpеделенную очеpедь select_wait. Имеет два аpгумента: wait_address Адpес указателя на wait_queue для помещения в циклический список запpосов. p Если p=NULL, select_wait бездействует, иначе текущий пpоцесс замоpаживается. wait пеpеносится из функции select(). Опpеделена в: linux/sched.h См. также: *sleep_on(), wake_up*(). *sleep_on() void sleep_on(stuct wait_queue **p) void interruptible_sleep_on(struct waitqueue **p) #include Замоpаживает пpоцесс до опpеделенного события, помещая инфоpмацию, тpебуемую для активизации, в wait_queue. sleep_on() используется в случае запpещенных пpеpываний, так что пpоцесс может быть запущен исключительно функцией wake_up(). interruptible_sleep_on() используется в случае замоpозки с pазpешенными пpеpываниями, когда пpоцесс может быть активизиpован опpеделенными сигналами, пеpеpывами pаботы дpугих пpоцессов. Используя wake_up_interruptible() вы можете активизиpовать пpоцесс с дальнейшим его исключением по отpаботке. Используют один аpгумент. p Указатель на заданную стpуктуpу wait_queue, в котоpую записывается инфоpмация для пpобуждения пpоцесса. Опpеделена в: kernel/sched.c См. также: select_wait(), wake_up*(). sti() #define sti()__asm__ __volatile__("sti"::) #include Разpешает неопознанные пpеpывания. sti - "SeT Interrupt enable" Опpеделена в asm/system.h См. также: cli(). sys_get*() int sys_getpid(void) int sys_getuid(void) int sys_getgid(void) int sys_geteuid(void) int sys_getegid(void) int sys_getppid(void) int sys_getpgrp(void) Эти системные вызовы могут быть использоваы для получения инфоpмации находящейся в таблице ниже или инфоpмации, котоpую можно получить пpямо из таблицы пpоцесса: foo=current->pid; pid ..... ID пpоцесса. uid ..... ID пользователя. gid ..... ID гpуппы. euid..... ID "эффективного" пользователя. egid..... ID "эффективной" гpуппы. ppid..... ID пpоpодителя пpоцесса. pgid..... ID пpоpодителя гpуппы. Системные вызовы не находят шиpокого пpименения, так как они не достаточно быстpы и тpебуют большого количества памяти. Поэтому они более не экспоpтиpуются как символы чеpез все ядpо. Опpеделена в: kernel/sched.c unregister_*dev() int unregister_chrdev(unsigned int major,const char *name) int unregister_blkdev(unsigned int major,const char *name) #include #include Аннулиpует pегистpацию дpайвеpа устpойства ядpом, позволяя последнему пеpедать основной номеp дpугому устpойству. Имеет два аpгумента. major Основной номеp заpегестpиpуемого pанее устpойства. Должен быть идентичен номеpу заданному register_*dev(). name Уникальная стpока идентифициpующая устpойство. Должно быть также идентична заданной в register_*dev(). Возвpащаемые значения: -EINVAL если основной номеp >= MAX.CHRDEV или MAX_BLKDEV (опpеделены в ), для символьных и блочных устpойств соответственно, если не имя или основной номеp не совпвдают с заданными пpи pегистpации. 0 в случае успеха. Опpеделена в fs/devices.c См. также: register_*dev(). wake_up*() void wake_up(struct wait_queue **p) void wake_up_interruptible(struct wait_queue **p) #include Активизиpуют пpоцесс, замоpоженный соответственной функцией *sleep_on(). wake_up() служит для активизации пpоцессов находящихся в очеpеди, где они могут быть помечены как TASK_INTERRUPTIBLE или TASK_UNINTERRUPTIBLE, в то вpемя как wake_up_interruptible() может активизиpовать пpоцессы лишь помеченные втоpой меткой, однако pаботает на поpядок быстpее wake_up(). Имеют один аpгумент: q указатель на стpуктуpу wait_queue, активизиpуемого пpоцесса. Помните что wake_up() не осуществляет пеpеключение задач, она лишь делает пpоцесс запускаемым для того, чтобы далее вызванная функция schedule() использовала его как пpетендента на выполнение. Опpеделена в kernel/sched.c См. также: select_wait(), *sleep_on(). 2.7. Написание драйвера SCSI. Copyright 1993 Rickard E. Faith(faith@cs.unc.edu). Все пpава заpезеpвиpованы. Пpедоставляется пpаво pаспpостpанения и создания копиий этого документа, если пpимечание об автоpских пpавах и это pазpешение сохpаняется на всех копиях. Здесь пpедставлена (с позволения автоpа) модифициpованная копия оpигинального документа. Если вы желаете воспpоизводить лишь эту часть книги, вы можете получить оpигинал по адpесу ftp.cs.unc.edu:/pub/faith/papers/scsi.paper.tar.gz 2.7.1. Зачем нужны драйверы SCSI. Ядро Linux содержит драйверы для следующих основных адаптеров SCSI: Adaptec 1542, adaptec 1740, Future Domain TMS-1660/TMS-1680, Segate ST-01/ST-02, Ultrastor 14F и Western Digital WD-7000.вы можете написать ваш собственный драйвер для неподдерживаемого адаптера. Также вы можете изменять готовые драйверы. Вступление к стандартному описанию SCSI-2 дает подробнейшее определение Small Computer System Interfase (Интерфейс Малых Компьютерных Систем) и об'ясняет, как SCSI-2 соотносится с SCSI-1 и CCS. Протокол SCSI создан для обеспечения эффективного обмена информацией с несколькими устройствами ( до 8 ) на нескольких адаптерах. Данные могут передаваться асинхронно со скоростью, определяемой характеристиками устройства и длиной кабеля. Синхронный обмен информацией может поддерживать скорость до 10 млн. передач в секунду. при использовании 32-битных шин скорость увеличивается до 40Мб в секунду. SCSI-2 содержит команды для магнитных, оптических дисков, стримеров, принтеров, процессоров, CD-ROMов, сканеров и коммуникационных устройств. В 1985 году первый стандарт SCSI стал национальным Американским Стандартом, и несколько производителей обратились к группе разработчиков X3T9.2 с с пожеланием расширить стандарт SCSI для использования полнодоступных устройств. В процессе расширения SCSI группа X3T9.2 разработала пакет, названный Common Comand SET (CCS - "общий набор команд") и создала несколько программных продуктов, базирующихся на этом интерфейсе. Параллельно этому группа занялась созданием расширенного станарта SCSI, названного SCSI-2. Он содержал в себе результаты разработок CCS с возможностью их использования различными устройствами. Также он включал в себя команды кеширования и другие не менее важные функции. Так как SCSI-2 был лишь более качественной расширенной копией стандарта SCSI-1, он обладал высокой степенью совместимости с устройствами SCSI-1. 2.7.2.1. Термины SCSI. "SCSI bus" - протокол обмена информацией с подключенными внешними устройствами SCSI. Одиночный обмен инициатора("initiator") с целью("target") может содержать до 8 слов ("phases"). Эти слова определяются целью (т.е. жестким диском). Текущее слово может быть определено путем просмотра пяти сигналов SCSI bus так, как это показано в таблице 1.1. Некоторые контроллеры (в частности, недорогой контроллер Seagate) требуют переделки сигналов, переданных SCSI bus, другие автоматически используют эти низкоуровневые сигналы. Каждое из 8 слов будет подробно описано. -SEL -BSY -MSG -C/D -I/O PHASE HI HI ? ? ? BUS FREE HI LO ? ? ? ARBITRATION I I&T ? ? ? SELECTION T I&T ? ? ? RESELECTION HI LO HI HI HI DATA OUT HI LO HI HI LO DATA IN HI LO HI LO HI COMMAND HI LO HI LO LO STATUS HI LO LO LO HI MESSAGE OUT HI LO LO LO LO MESSAGE IN I = сигнал инициатора; T = сигнал цели; ? = HI или LO Таблица 1.1. Определение слов SCSI Bus. Слово BUS FREE Определяет SCSI bus как незанятый. Слово ARBITRATION Подается в случае, если устройство SCSI пытается установить контроль над SCSI bus.В этот момент устройство вносит свой SCSI ID в DATA BUS (установки SCSI bus).Например, если ID = 2, устройство задает дате 0x04. В случае попытки обращения нескольких устройств одновременно, над целью устанавливает контроль устройство с наиболее высоким ID.Слово ARBITRATION использовалось также в стандарте SCSI-1. Слово SELECTION После установки контроля устройство, ставшее инициатором, заносит в дату протокола передачи SCSI ID цели. Если цель обнаруживается, она определяется, как занятая с помощью строки -BSY. Эта строка остается активной все то время, пока цель соединена с инициатором. Слово RESELECTION Протокол SCSI позволяет устройству отключаться от протокола передачи во время работы запроса. Когда устройство готово к продолжению обмена, оно вновь подключается к адаптеру. Слово RESELECTION идентично слову SELECTION, за исключением того, что оно используется отключенной целью для подключения к исходному инициатору. Драйверы, не поддерживающие RESELECTION, не имеют возможности раз'единения с целью SCSI. Однако RESELECTION поддерживается почти всеми драйверами, так что многозадачные многозадачные устройства SCSI выполнять одновременно несколько задач, что уменьшает время обмена при запросах ввода/вывода. Слово COMMAND После этого слова отинициатора к цели может передаваться 6-ти, 10-ти и 12-ти байтная команда. Слова DATA OUT и DATA IN После этих слов осуществляется непосредственная передача информации между целью и инициатором. В случае DATA OUT, например, информация передается от адаптера к диску. DATA IN в таком случае осуществляет обратную передачу. Если команда SCSI требует передачи информации, слово не используется. Слово STATUS Это слово задается после завершения всех команд и дает возможность послать инициатору статусный байт. Существует 9 вариантов статусного байта (таблица 1.2). Заметим, что так как для статусного кода используются биты 1-5, статусный байт перед использованием маскируется 0x3e. Значения важнейших статусных кодов: GOOD - операция выполнена успешно. CHECK CONDITION - сообщение о случившейся ошибке.Команда REQUEST SENSE может быть использована для получения более подробной информации об ошибке. BUSY - устройство не может выполнить комаду. Это может случиться во время самотестирования или сразу после включения устройства. Слова MESSAGE OUT и MESSAGE IN Дополнительная информация передается между инициатором и целью. Этой информацией может быть статус посторонней команды или запрос Value Status 0x00 GOOD 0x02 CHECK CONDITION 0x04 CONDITION MET 0x08 BUSY 0x10 INTERMEDIATE 0x14 INTERMEDIATE-CONDITION MET 0x18 RESERVATION CONFLICT 0x22 COMMAND TERMINATED 0x28 QUEUE FULL (После наложения маски 0x3e) Таблица 1.2. Статусные коды SCSI. для смены протокола. Слова MESSAGE OUT и MESSAGE IN могут неоднократно встречаться во время одной передачи.Если во время передачи доступно использование RESELECTION, драйвер должен поддерживать также слова SAVE DATA POINTERS, RESTORE POINTERS и DISCONNECT (сохранение и загрузка указателей, раз'единение). В SCSI-2 не все драйверы сохраняют указатели перед раз'единением. 2.7.3. Команды SCSI. Каждая команда SCSI имеет длину 6,10 или 12 байт. нижеперечисленные команды должны быть качественно изучены будущими разработчиками драйверов SCSI: REQUEST SENSE Когда команда возвращает статус CHECK KONDITION, предусмотренная в Linux подпрограмма высокого уровня автоматически запрашивает более подробную информацию об ошибке, подавая команду REQUEST SENSE. Эта команда возвращает ключ и код ошибки ( называемый также "addtitional sense code"(ASC)- дополнительный смысловой код ). 16 возможных ключей описаны в таблице 1.3. Для получения информации о ASC, а также об ASCQ ("additional sense code qualiter"- дополнительный спецификатор смыслового значения кода), возвращаемом некоторыми драйверами, обращайтесь к стандарту SCSI[ANS] или к техническому руководству SCSI. Ключ Описание 0x00 NO SENSE (НЕТ ОТВЕТА) 0x01 RECOVERED ERROR (ВСКРЫТАЯ ОШИБКА) 0x02 NOT READY (НЕ ГОТОВ) 0x03 MEDIUM ERROR (СРЕДНЯЯ ОШИБКА) 0x04 HARDWARE ERROR (ОШИБКА АППАРАТНОГО ОБЕСПЕЧЕНИЯ) 0x05 ILLEGAL REQUEST (НЕПРАВИЛЬНЫЙ ЗАПРОС) 0x06 UNIT ATTENTION (ПРЕДУПРЕЖДЕНИЕ) 0x07 DATA PROTECT (ЗАЩИЩЕННАЯ ИНФОРМАЦИЯ) 0x08 BLANK CHECK (ПРОВЕРКА НА ОТСУТСТВИЕ ИНФОРМАЦИИ) 0x09 (Vendor specific error) (Ошибка инициатора) 0x0a COPY ABORTED (ПРЕКРАЩЕННОЕ КОПИРОВАНИЕ) 0x0b ABORTED COMMAND (ПРЕКРАЩЕННАЯ КОМАНДА) 0x0c EQUAL (ЭКВИВАЛЕНТНОСТЬ) 0x0d VOLUME OVERFLOV (ПЕРЕПОЛНЕНИЕ) 0x0e MISCOMPARE (НЕСООТВЕТСТВИЕ) 0x0f RESERVED (ЗАРЕЗЕРВИРОВАНО) Таблица 3.1. Значения смысловых ключей. TEST UNIT READY Эта команда для тестирования статуса цели. Если цель может воспринимать команды среднего доступа (READ, WRITE),команда возвращает статус GOOD, в ином случае возвращается статус CHECK CONDITION и смысловой ключ NOT READY. Последнее обычно говорит о происходящем в настоящий момент самотестировании цели. INQUIRY Эта команда возвращает модель, производителя и тип устройства цели. Высокоуровневый Linux использует эту команду для определения разницы между оптическими, магнитными дисками и стримерами (высокоуровневый Linux не управляет принтерами, процессорами, или автоматическими устройствами). READ и WRITE Эти команды передачи информации от и к цели. До использования READ и WRITE вы должны убедиться в том, что ваш драйвер обладает возможностью поддержки простейших команд, таких, как TEST UNIT READY и INQUIRY. 2.7.4. С чего начинать ? Авторы низкоуровневых драйверов устройств должны представлять себе, как управляет прерываниями ядро. Как минимум, вами должны быть изучены функции, которые разрешают (sti()) и запрещают(cli()) прерывания. Также для некоторых драйверов нужны функции определения времени вызова функций schedule(), sleepon() и wakeup(). В разделе 2.6 вы можете встретить более подробное описание этих функций. 2.7.5. Введение: сбор инструментов. До того, как вы начнете писать драйвер SCSI для Linux, вам придется достать некоторые инструменты (ресурсы). Самое важное - системный диск с системой Linux, желательно, жесткий диск с интерфейсом IDE, RLL или MFM. Во время разработки вашего SCSI придется много раз перестраивать ядро и перезапускать систему. Ошибки программирования могут привести к уничтожению информации на вашем диске SCSI, а также на посторонних носителях. Сохраняйте информацию на дисках! Установленная система Linux может быть минимизирована: вы можете ограничиться библиотеками и утилитами компилятора GCC, текстовым редактором и текстом ядра. Также будут полезны дополнительные инструменты od, hexdump и less. Все эти программы свободно помещаются на диске размером 20 -30Мб. Также вам потребуется подробная документация. Как минимум, вам нужно описание используемого вами адаптера. Так как Linux распространяется свободно, и так как вы тоже пожелаете поделиться с другими вашими разработками, существуют нагласные соглашения, согласно которым, если если вы хотите обнародовать вашу подпрограмму, к ней должен быть приложен об'ектный код; однако на данном этапе это не всегда случается. Вам будет полезно описание стандарта SCSI. Описание жесткого диска обычно не требуется. Прежде, чем начать, сохраните копии файлов hosts.h и scsi.h, а также одного из существующих драйверов ядра Linux. Это будет полезной рекомендацией во время написания. 2.7.6. Интерфейс SCSI в Linux. Высокоуровневый интерфейс SCSI ядра Linux управляет всеми взаимодействиями ядра и низкоуровневых драйверов устройств. Благодаря своим основательным разработкам, драйверы SCSI требуют лишь небольшого содействия высокоуровневого кода. Автор драйвера низкого уровня, не желающий детально разбирать принципы системы ввода/вывода ядра, может написать драйвер в кратчайшие сроки. Две основные структуры (Scsi_Host и Scsi_Cmnd) используются для связывания высокоуровневого кода и кода низкого уровня. Следующие два параграфа являются детальными описаниями этих структур и требований драйвера низкого уровня. 2.7.6. Структура Scsi_Host. Структура Scsi_Host служит для описания драйвера низкого уровня коду высокого. Обычно это описание помещается в главный файл драйвера устройства в препроцессорные определения, как показано на рис. 1.1. Структура Scsi_Host представлена на рис. 1.2 Каждое из полей будет дале подробно об'яснено. #deflne FDOMAIN_16X0 { "Future Domain TMC-16x0", \ fdomain-16x0_detect, \ fdomain_16x0_info, \ fdomain_16x0_command, \ fdomain_16x0_queue, \ fdomain_16x0_abort, \ fdomain_16x0_reset, \ NULL, \ fdomain_16x0_biosparam, \ 1, 6, 64, 1,0, 0} #endif Рис 1.1: Основной файл драйвера устройства. typedef struct { char *name; int (* detect) (int); const char *(* info)(void); int (* queuecommand)(Scsi_Cmnd *, void (*done)(Scsi_Cmnd *)); int (* command) (Scsi_Cmnd *); int (* abort) (Scsi_Cmnd *, int); int (* reset) (void); int (* slave_attach) (int, int); int (* bios_param)(int, int, int []); int can_queue; int this_id; short unsigned int sg_tablesize; short cmd_per_lun; unsigned present:1; unsigned unchecked_isa_dma:1; } Scs i_Host; Рис.1.2: Структура Scsi_Host. 2.7.7.1. Переменные в структуре Scsi_Host. В общем случае переменные в структуре Scsi_Host не используются до вызова функции detect(), так как некоторым переменным может присваиваться значение лишь во время определения (обнаружения) адаптера. Это происходит в случае, если драйвер может управлять несколькими устройствами с похожими свойствами, так что некоторые параметры структуры зависят от обнаруженного адаптера. 2.7.7.1.1. name name содержит указатель на краткое описание host адаптера SCSI. 2.7.7.1.2. can_queue can_queue содержит число невыполненных команд, которые может выполнить главный адаптер. В случае, если ваш драйвер поддерживает слово RESELECTION и использует прерывания, этой переменной присваивается значение 1. 2.7.7.1.3. this_id Большинство главных адаптеров имеют особые, приписанные им SCSI ID. Эти SCSI ID, обычно равные 6 или 7, используются для реализации RESELECTION. this_id содержит SCSI ID адаптера. Если адаптеру не соответствует ID, этой переменной присваивается значение -1 (RESELECTION в таком случае не поддерживается). 2.7.7.1.4. sg_tablesize Высокоуровневый код поддерживает метод "scatter-gather" (компановка - раз'единение) повышения эффективности обмена информацией с помощью комбинирования многих маленьких запросов в несколько больших. Так как большинство накопителей SCSI форматированы с прослойкой 1:1, что означает, что все сектора на одной дорожке располагаются последовательно, время, требуемое для выполнения слов ARBITRATION и SELECTION, не превышает времени чередования секторов.Так что за один оборот диска может сработать лишь один процесс, что приводит к скорости передачи 50Кб в секунду, в то время, как метод "scatter-gather" дает скорость около 500Кб в секунду. sg_tablesize содержит максимально возможное число запросов в списке метода компановки-раз'единения. Если драйвер не поддерживает метод "scatter-gather", этой переменной присваивается значение SG_NONE. Если драйвер поддерживает неограниченное число групповых запросов, эта переменная принимает значение SG_ALL. В некоторых драйверах это число ограничивается предельным значением sg_tablesize, поддерживаемым адаптером. Некоторые адаптеры Adaptec требуют значение не более 16. 2.7.7.1.5. cmd_per_lun SCSI стандарт поддерживает понятие "компановка команд". Компановка команд позволяет нескольким командам выстраиваться в порядке очередности к подаче на одно устройство. Эта переменная равна 1 в случае поддержки компановки команд. Однако на данный момент высокоуровневый код SCSI не использует преимуществ, предоставляемых этой возможностью. Скомпанованные команды имеют фундаментальные отличия от команд одиночных (что описывается в переменной can_queue). Скомпанованные команды всегда предназначаются одной и той же цели и не обязательно используют слово RESELECTION. Также компанованные команды исключают слова ARBITRATION, SELECTION и MESSAGE OUT после прохождения первой установленной в списке. В то же время одиночные команды могут посылаться на контролируемую цель и требуют слова ARBITRATION, SELECTION, MESSAGE OUT и RESELECTION. 2.7.7.1.6. present Бит present устанавливается в случае обнаружения устройства. 2.7.7.1.7. unchecked_isa_dma Некоторые host - адаптеры используют доступ к указанной памяти (Direct Memory Acess(DMA)) для чтения и записи блочной информации прямо в основаную память компьютера. Linux - система виртуальной памяти,имеющая возможность использовать более 16Мб физической памяти. На машинах с шиной ISA DMA ограничен шестнадцатью Мб физической памяти. Если установлен бит unchecked_isa_dma, высокоуровневый код будет поддерживать информационный буфер адресацией ниже 16Мб физической памяти. Драйверы, не используюшие DMA, устанавливают бит в 0. Драйверы, работающие с шиной EISA, всегда устанавливают этот бит также в 0, так как машины с EISA не позволяют доступа к DMA. 2.7.7.2. Функции структуры Scsi_Host. 2.7.7.2.1. detect() Единственный аргумент функции detect() - "главный номер"(host number), индекс к переменным Scsi_hosts (массив типа struct Scsi_Host). Функция detect() возвращает ненулевое значение в случае обнаружения адаптера и нулевое в обратном случае. Определение главного (host) адаптера должно производиться очень аккуратно. Обычно процесс начинается с просмотра области ROM в поисках "описания BIOS" главного адаптера. В PS/AT и совместимых компьютерах адресное пространство с адреса 0xc0000 по 0xfffff полностью распределено. Видео-BIOS компьютера расположена начиная с адреса 0xc0000, BIOS жесткого диска, если таковой существует, начинается с адреса 0xc8000. Во время загрузки PS/AT - совместимых компьютеров каждый 2-х килобайтный блок с адреса 0xc0000 до 0xf8000 проверяется на 2-х байтовую запись 0x55aa, которая свидетельствует о существовании расширенного BIOS. Описание BIOS обычно содержит серию из нескольких байт, идентифицирующих BIOS. Future Domain Bios, например, имеет описание: FUTURE DOMAIN CORP. (C) 1986 - 1990 1800 - V2.07/28/89 Оно начинается с пятого байта от начала блока BIOS. После обнаружения описания BIOS можно оттестировать функциональные качества адаптера особыми способами. Так как описания BIOS жестко закодированы в ядре, смена BIOS может привести драйвер к сбою. У пользователей адаптера SCSI исключительно в Linux может возникнуть желание отключить BIOS для ускорения начальной загрузки. По этим причинам должен существовать альтернативный метод определения адаптера. Обычно каждый адаптер имеет несколько адресов ввода/вывода, использующихся для обеспечения связи. Иногда эти адреса жестко определены в драйвере, заставляя пользователей Linux, имеющих подобный адаптер, использовать определенную установку адресов. Другие драйверы сами определяют эти адреса, просматривая все возможные. Обычно адаптер позволяет использовать 3 - 4 набора, руководствуясь переключателями на карте. После определения адресов портов ввода/вывода адаптер может сам заявлять о себе. Эти тесты особенны для каждого адаптера, но имеют общие методы определения основного адреса BIOS (который затем может быть сравнен с адресм BIOS, найденным во время поиска определения BIOS)для проверки уникального номера, присущего карте. На машинах с шиной MCA каждому типу карты дается уникальный номер, благодаря которому ни один посторонний производитель не может использовать некоторые адаптеры. Future Domain, например, используют эту технологию на машинах ISA. 2.7.7.2.1.1. Запрос IRQ. После определения detect() должен запросить канал DMA и пириоритет прерывания. Всего существует 16 приоритетов, называемых IRQ - от 0 до 15. Ядро поддерживает два метода установки обработчика IRQ: irqaction() и request_irq(). Функция request_irq() запрашивает два аргумента: номер IRQ и указатель на подпрограмму-обработчика. Часто устанавливаются параметры структуры sigaction с использованием irqaction(). Текст request_irq() показан на рисунке 1.3. Определение функции irqaction(): int irqaction( unsigned int irq, struct sigaction *new) где первый параметр, irq, номер запрошенного IRQ, второй, new, структура, определение которой показано на рис. 1.4. int request_irq( unsigned int irq, void (*handler)( int )) { struct sigaction sa; sa.sa_handler = handler; sa.sa_flags = 0; sa.sa_mask = 0; sa.sa_restorer = NULL; return irqaction( lrq, &sa ); } Рис. 1.3: Функция request-irq(). struct sigaction { __sighandler_t sa_handler; sigset_t sa_mask; int sa_flags; void (*sa_restorer) (void); }; Рис. 1.4: Структура sigaction sa_handler в этой структуре указывает на подпрограмму обработчика прерываний, определяемую void fdomain_16x0_intr( int irq ) где irq - номер IRQ, указывающий обработчику на пробуждение. Переменная sa_mask используется как глобальный флаг подпрограммы irqaction(). Переменная sa_flags может быть установлена либо в 0, либо в SA_INTERRUPT. Если выбран 0, обработчик прерываний запускается при разрешенных посторонних прерываниях и возвращает значение через сигнальные функции обработчика. Эта установка используется для низких IRQ, таких, как таймер и клавиатура. SA_INTEERUPT используется при больших ("быстрых") IRQ, например, при использовании упраляемых прерываниями драйверов жестких дисков. В последнем случае обработчик вызывается с запрещенными прерываниями. Переменная sa_restorer в данный момент не задействована и традиционно установлена в NULL. Функции request_irq() и irqaction() будут возвращать нуль, если IRQ успешно поставлен в соответствие определенному обработчику прерываний. Ненулевые возвращаемые значения могут быть следующими: - EINVAL Запрошенный IRQ больше 15, или обработчику прерываний был подан указатель на NULL. - EBUSY Запрошенный IRQ уже занят другим обработчиком прерываний. Эта ситуация не возникает в случае использования panic(). Ядро использует Intel "распределение" для установки IRQ, запрашиваемых функцией irqaction(). 2.7.7.2.2. Запрос канала DMA. Некоторые адаптеры SCSI используют DMA лдя помещения больших информационных блоков в памяти. Так как процессор не управляет передачей информации в блоки DMA, передача осуществляется быстрее передачи, контролируемой процессором и позволяет последнему работать в это время над другой задачей. Адаптеры используют определенные каналы DMA. Эти каналы определяются функцией detect() и запрашиваются ядром с помощью request_dma(). Эта функция получает номер канала DMA как свой единственный параметр и возвращает нуль, если канал DMA успешно подключен. Другие возможные возвращаемые значения: - EINVAL Запрошенный канал DMA имеет номер больше 7. - EBUSY Запрошенный канал DMA уже используется. Этой ситуация может привести к неудовлетворения запроса SCSI. В этом случае также можно использовать panic(). 2.7.7.2.3. info() Функция info() возвращает указатель на статическую область, содержащую описание драйвера низкого уровня. Это описание содержится в переменной-указателе name и выводится во время загрузки. 2.7.7.2.4. queuecommand() Функция queuecommand() осуществляет запуск команды SCSI адаптером, затем завершает работу. По завершению команды вызывается функция done() с указателем на структуру Scsi_Cmnd в качестве параметра. Это позволяет команде SCSI запуститься в режиме прерывания. Перед завершением работы функция queuecommand() должна выполнить следующие операции: 1. Сохранить указатель на структуру Scsi_Cmnd. 2. Сохранить указатель на функцию done() в качестве поля Scsi_done() в структуре Scsi_Cmnd. См. раздел 2.7.7.2.5 для более подробной информации. 3. Установить специальные переменные в Scsi_Cmnd, требуемые драйвером. 4. Запустить команду SCSI. Для расширенных host-адаптеров это может быть простейшая засылка команды в "mailbox" host-адаптера. Для менее "мудрых" адаптеров используется сначала слово ARBITRATION. Функция queuecommand() вызывается лишь в случае ненулевой переменной can_queue (см. 2.7.7.1.2). В ином случае для всех запросов используется функция command(). В случае успеха функция queuecommand() возвращает 0. (Высокоуровневый код SCSI игнорирует это возвращаемое значение). 2.7.7.2.5. done() Функция done() вызывается после завершения команды SCSI. Единственный параметр, этой функции - указатель на структуру Scsi_Cmnd, используемую прежде функцией queuecommand(). Перед вызовом функции done() должна быть правильно установлена переменная result. Она имеет тип 32-битного целого, каждый байт которого имеет свое значение: Байт 0 - Содержит код SCSI STATUS, как описано в 2.7.2.1. 1 - Содерит SCSI MESSAGE, как описано в 2.7.2.1 2 - Содержит возвращаемый код host адаптера. Этим кодам присваивается значения в scsi.h: DID_OK Ошибок не обнаружено DID_NO_CONNECT SCSI SELECTION не может передаться из-за отсутствия устройства по указанному адресу. DID_BUS_BUSY Ошибка SCSI ARBITRATION DID_TIME_OUT Произошла приостановка работы процесса по неизвестной причине, возможно во время SELECTION или в ожидании RESELECTION. DID_BAD_TARGET SCSI ID цели такой-же как ID адаптера DID_ABORT Высоко-уровневый код вызывает низко-уровневую функцию abort(). DID_PARITY Ошибка SCSI PARITY DID_ERROR Ошибка, не поддающаяся распознанию (к примеру ошибка самого адаптера) DID_RESET Высоко-уровневый код вызывает низко-уровневую функцию reset() DID_BAD_INTR Возникновение непредвиденного прерывания, которым не возожно управлять. Возврат DID_BUS_BUSY будет пытаться запустить команду еще раз, в то время как DID_NO_CONNECT сбросит команду. Байт 3 Этот байт предназначен для возвращения кода высокого уровня и устанавливается низким уровнем в 0. В настоящий момент драйвера низкого уровня не описывают сообщения об ошибках, поэтому легче всего для вас найти их определения в scsi.c вместо того чтобы исследовать существующие драйвера. 2.7.7.2.6 command() Функция command() запускает команду SCSI и возвращается после ее завершения. Когда был создан оригинал кода SCSI, в нем не осуществлялась поддержка драйверов управляемых прерываниями. Старые драйвера менее эфеективны чем созданные на данный момент драйвера управляемые прерываниями, но более просты в написании. Для новых драйверов эта функция заменена на queuecommand(), как описано в следующей программе: ststic volatile int internal_done_flag = 0; static volatile int internal_done_errcode = 0; static void internal_done(Scsi_Cmnd *SCpnt); { internal_done_errcode = SCpnt->result; ++internal_done_flag; } int aha1542_command(Scsi_Cmnd *SCpnt) { aha1542_queuecommand (SCpnt, internal_done ); while(!internal_done_flag); internal_done_flag = 0; return internal_done_errcode; } Возвращаемое значение - то же, что и в переменной result в структуре Scsi_Cmnd. См 2.7.7.2.5 и 2.7.8. 2.7.7.2.7 abort() Высокоуровневый код SCSI управляет всеми преостановками. Это освобождает драйвер низкого уровня от распределения времени между запросами на периоды исполнения для различных устройств (преостановка работы стримера может быть на много дольше, нежели преостановка жесткого диска). Функция abort() используется отключения запроса текущей команды SCSI определенной указателем Scsi_Cmnd. После установки переменной result в структуре Scsi_Cmnd функция abort() возвращает нулевое значение. Если code, второй параметр функции abort(), равен нулю, тогда result устанавливается в DID_ABORT. В ином случае result равн code (обычно это DID_TIM_OUT и DID_RESET). На данный момент ни один из драйверов низкого уровня не может правильно отключать комманды SCSI. Инициатор должен запрашивать словом MESSEGE OUT цель, для решения этой задачи. Затем инициатор посылает ABORT цели. 2.7.7.2.8 reset() Функция reset() служит для выгрузки шины SCSI. После выгрузки ни комманда SCSI не будет выполняться, возвращая код DID_RESET. В настоящий момент ни один из драйверов низкого уровня не может правильно пользоваться этой операцией. Для правильной выгрузки инициатор запрашивает (посылая -ATN) MESSAGE OUT, и подает цели команду BUS DEVICE RESET. Можно также дать команду SCSI RESET, спослав -RST, заставляющую все цели отключиться. После выгрузки будет полезно удалить также протокол связи. 2.7.7.2.9 slave_attach() Функция на данный момент не описана. Используется для установки связи между host адаптером и целью. Связь подразумевает обмен парой SYNCHRONOUS DATA TRANSFER REQUEST между целью и инитатором. Обмен возникает при условиях: - Устройство SCSI поддерживающее обмен не соединяется с устройством, после получения сигнала отбоя (RESET). - Устройство SCSI также не может быть соединено с другим в случае если оно получило сообщени BUS DEVICE RESET. 2.7.7.2.10 bios_param() Linux поддерживает систему деления жеского диска MS-DOS. Каждый диск содержит "таблицу частей" в которой определено как диск разбит на логические диски. Обработка информации в таблице требует знания о размере диска в циллиндрах, головках и секторах. Диски SCSI скрывают свои физические параметры и логически представляются списком секторов. Для получения совместимости с MS-DOS, host адаптер SCSI "лжет" о своих физических параметрах. Так что вместо параметров физических устройство SCSI посавляет "логические параметры". Linux нуждается в определении "логических параметров" для правильного изменеия таблицы. В сущности метода конвертации логических параметров в физические не существует. Функция bios_param() представляет собой осуществление доступа к параметрам. Параметр size содержит размер диска в секторах. Некоторые host адаптеры располагают формулой для подсчета логических параметров исходя из этой цифры, иным приходится хранить информацию в таблицах доступных драйверу. Для обеспечения этого доступа, параметр dev хранит информацию о номере устройства. Два макроса описанные в linux/fs.h осуществляют определение этого значения: MAJOR(dev) - основного номера устройства и MINOR(dev) - определение подномера. Это те-же номера, используемые при выполнении стандартной команды Linux mknod, служащей для создания устройства в каталоге /dev. Параметр info указывает на массив целых, заполняемый функцией bios_param() до возвращения: info[0] Количество головок info[1] Количество секторов на циллиндр info[2] Количество циллиндров Информация в info является "логиескими парвметрами" устройстваб, используемые методами MS-DOS как физические. 2.7.8 Структура Scsi_Cmnd Структура Scsi_Cmnd, как показано на рисунке 1.6 использует код высокого уровня для спецификации комманды SCSI для запуска низко-уровневым кодом. Множество переменных в структуре Scsi_Cmnd могут не использоваться в драйвере низкого уровня. 2.7.8.1 Зарезервированная область 2.7.8.1.1 Информационные переменные. host - индекс массива scsi_hosts. target - cодержит ID цели команды SCSI. Эта информация важна в случае поддержки целью многозадачности. cmnd - массив байт, содержащий текущую команду SCSI. Эти байты посылаются цели посте строки COMMAND. cmnd[0] - код команды SCSI. Макро COMMAND_SIZE, определенный в scsi.h используется для определения длины команды. result - код результата запроса SCSI. Cм. 2.7.7.2.5 для более подробной информации об этой переменной. Она должна быть верно установлена до возврата низкоуровневых подпрограмм. 2.7.8.1.2 Список Разветвления - компановки. (Scatter-gather) use_sg содержит количество кусков обрабатываемых scatter-gather. Если use_sg = 0, тогда request_buffer указывает на буфер данных команды SCSI, и размер буфера содержится в request_bufferlen. В ином случае request_buffer указывает на массив структур scatterlist и use_sg идентифицирует количество структур в массиве. Использование request_buffer довольно тяжело. Каждый элемент массива scatterlist содержит компоненты address и length. Если флаг unchecked_isa_dma в структуре scsi_Host установлен в 1, адрес гарантированно попадает в область первых 16Мб физической памяти. Одной SCSI командой можно в таком случае передать большое количество информации, при этом длина большого куска равна сумме длин всех малых. typedef struct scsi_cmnd { int host; unsigned char target; lun; index; struct scsi_cmnd *next, *prev; unsigned char cmnd[10]; unsigned request_bufflen; void *request_buffer; unsigned char data_cmnd[10]; unsigned short use_sg; unsigned short sglist_len; unsigned bufflen; void *buffer; struct request request; unsigned char sense_buffer[16]; int retries; int allowed; int timeout_per_command, timeout-total, timeout; unsigned char internal_timeout; unsigned flags; void (*scsi_done)(struct scsi_cmnd *); void (*done)(struct scsi_cmnd *); Scsi_Pointer Scp; unsigned char *host_schribble; int result; } Scsi_Cmnd; Рис. 1.6: Структура Scsi_Cmnd. 2.7.8.2. Рабочие области. В зависимости от возможостей и требований host адаптера, список scatter- gather может управляться различными способами. Для поддержки многозадачности несколько рабочих областей прикрепляются эксклюзивно к драйверу низкого уровня. 2.7.8.2.1 Указатель scsi_done(). Указатель должен быть установлен на функцию done() в функции queuecommand(). Других использований этому указатенлю не предусмотрено. 2.7.8.2.2 Указатель host_scribble Код высокого уровня поддерживает пару функций распределения памяти - scsi_malloc() и scsi_free(), которые гарантируют возврат физической памяти из первых 16Мб. Эта память также подходит для использования DMA. Количество распределенной памяти под запрос должно быть кратно 512 байтам и быть не больше 4096 байт. Общее количество памяти доступной scsi_malloc() определяется арифметической функцией с тремя аргументами, находящиеся в Scsi_Host - переменные sg_tablesize,cmd_per_lun и unchecked_isa_dma. Указатель host_scribble указывает на область досупной памяти выделенной scsi_malloc(). Драйвер SCSI низкого уровня обладает возможностью управления этим указателем и соответствующей ему памяти, а также возможностью очистки ненужной информации в памяти. 2.7.8.2.3 Структура Scsi_Pointer. Переменная SCp, структура типа Scsi_Pointer, описана на рисунке ниже. Переменные этой структуры могут быть использованы любыми средствами в драйверах низкого уровня. Как обычно buffer здесь указывает на текущую позицию scatterlist, buffer_residual показывает количество элементов находящихся в scatterlist, ptr - указатель на буффер, а this_residual - число символов для передачи. Некоторые host адаптеры требуют эту информацию, некоторые игнорируют ее. Второй набор переменных содержит информацию о статусе SCSI, различные указатели и флаги. typedef struct scsi_pointer { char *ptr; int this_residual; struct scatterlist *buffer; int buffers_residual; volatile int Status; volatile int Message; volatile int have_data_in; volatile int sent_command; volatile int phase; }Scsi_Pointer;