Партнерка на США и Канаду по недвижимости, выплаты в крипто
- 30% recurring commission
- Выплаты в USDT
- Вывод каждую неделю
- Комиссия до 5 лет за каждого referral
Такая ситуация носит название race condition (условия состязания, или состязание). Результат зависит от того, кто начинает первым. Процессы A и B могут быть запущены на разных процессорах, и их взаимодействие может вызвать такую проблему.
Семафоры представляют собой основной механизм доступа к ресурсам. Простейший вариант – бинарный семафор=мьютекс (MUTual EXclusion – взаимоисключения). Процессы, использующие семафоры в режиме мьютесков предотвращают одновременный запуск одного и того же кода, или доступ к одним и тем же данным.
struct semaphore sem;
Семафоры в Linux определены в заголовочном файле <asm/semaphore. h>. Они описаны в структуре semaphore, и драйвер обязан использовать эту структуру только через предлагаемый к ней интерфейс.
Перед использованием надо инициализировать семафор, - передачей числового аргумента в функцию sema_init(). Для приложений, использующих мьютексы (т. е. для приложений, которые должны предотвратить одновременный доступ к одним и тем же данным), семафоры должны быть проинициализированны значением 1, которое означает разрешение работы семафора. sema_init (&sem, 1);
Процесс, желающий использовать код защищенный семафором, должен сначала проверить, что данный код не используется другим процессом. Доступ к семафору: down() и down_interruptible(&sem).
Эти функции проверяют значение семафора на величину большую нуля. Если это так, то функция декрементирует значение семафора и завершается. Если значение семафора равно нулю, то функция засыпает и просыпается через некоторое время, когда некий другой процесс, использующий семафор, предположительно, освобождает его.
Функция down_interruptible() может быть прервана сигналом, в то время как функция down() не принимает сигналы к процессу. Сложность использования сигнальных прерываний заключается в том, что в теле функции down_interruptible() необходимо всегда проверять, была ли функция прервана. Обычно, функция возвращает 0 в случае успеха, и не ноль в случае неудачного завершения. Если процесс прерывается, то он не получит семафоров, и, таким образом, нет необходимости вызывать функцию up(). Поэтому, типичный вызов получения семафора выглядит следующим образом:
if (down_interruptible (&sem)) return - ERESTARTSYS;
Возвращаемое значение - ERESTARTSYS говорит системе, что операция была прервана сигналом. Процесс, который получает семафор должен всегда освобождать его впоследствии.
Освобождение семафора: up (&sem);
Этот код увеличивает значение семафора и будит все процессы, которые ожидают доступного семафора. Во избежании тупиковых ситуаций, получением семафора должны заниматься только методы драйвера. Внутренние процедуры, должны предполагать, что семафор уже получен. При соблюдении этих условий, доступ к структуре mydrv будет корректным, и не приведет к ситуации “race conditions”.
25. Функция управления ioctl: ее описание в структуре file_operations и прототип в режиме ядра.
ioctl - сокращение от Input Output ConTroL.
Любое устройство может иметь свои команды ioctl, которые могут читать (для передачи данных от процесса ядру), писать (для передачи данных от ядра к процессу), и писать и читать, и ни то ни другое.
Функция ioctl() драйвера получает аргументы согласно следующему объявлению:
int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);
Указатели inode и filp представляют собой значения соответствующие файловому дескриптору fd, переданному пользовательским процессом, и полностью совпадают с параметрами, передаваемыми в системный вызов open().
Аргумент cmd передается от пользователя неизменным,
необязательный аргумент arg передается в форме unsigned long, что может соответствовать как целому значению, так и указателю. Если вызывающая программа не передает третий аргумент, то значение arg, полученное драйвером имеет неопределенное значение.
-) проверка типов передаваемых аргументов запрещена, поэтому компилятор не сможет предупредить о возможной ошибке, (до момента исполнения ошибочного кода).
Реализация метода ioctl(), в большинстве случаев строится на основе оператора switch, который обеспечивает ветвление, согласно значению аргумента cmd. Различные команды имеют различные числовые значения, которым, обычно, дают символические имена для улучшения читабельности кода. Символические имена задаются через директиву define препроцессора. Обычно, такие имена задаются в заголовочных файлах драйвера. Если пользовательская программа, хочет использовать те же символические имена, то достаточно подключить к ней соответствующий заголовочный файл.
26. Генерирование номера команды функции ioctl. Макросы _IOW, _IO, _IOR.
Номер ioctl содержит комбинацию бит, составляющих старший номер устройства, тип команды и тип дополнительного параметра. Обычно номер ioctl создается макроопределением (_IO, _IOR, _IOW или _IOWR, в зависимости от типа) в файле заголовка. Этот заголовочный должен подключаться директивой #include, к исходным файлам программы, которая использует ioctl для обмена данными с модулем.
Если предполагается использовать ioctl в собственных модулях, то следует обратиться к файлу Documentation/ioctl-number. txt с тем, чтобы не "занять" зарегистрированные номера ioctl.
В заголовочном файле определяется использование битовых полей: тип ("магический номер"), порядковый номер команды, направление передачи и размер аргумента. Файл ioctl-number. txt содержит список "магических номеров" используемых в ядре. Необходимо выбрать номер отличающийся от зарезервированных в файле, для избежания перекрытия номеров. Также, в этом текстовом файле содержится список причин, по которым, данное соглашение о нумерации кодов команд должно быть использовано.
Конечно же, и приложение, и драйвер должны использовать одно и тоже соглашение о нумерации кодов команд. Выбрав способ нумерации остается только организовать оператор switch в драйвере.
Новый способ определения номеров команд использует четыре битовых поля, которые имеют описанное ниже значения. Все новые, используемые здесь, макросимволы определены в <linux/ioctl. h>.
type Магическое число. Выбор номера необходимо осуществлять после ознакомления с файлом ioctl-number. txt. Именно этот выбранный номер, необходимо, в дальнейшем, использовать везде, где он потребуется для вашего драйвера. Данное поле имеет емкость в восемь бит (_IOC_TYPEBITS).
number Порядковый номер. Емкость – восемь бит (_IOC_NRBITS).
direction Направление передачи данных, если данная команда вызывает пересылку данных. Возможные значения: _IOC_NONE (нет передачи данных), _IOC_READ, _IOC_WRITE, и _IOC_READ | _IOC_WRITE (данные передаются в обоих направлениях). Точкой наблюдения передачи данных является приложение. Таким образом _IOC_READ означает чтение данных из устройства, так, что драйвер должен писать в пользовательский процесс. Обратите внимание, что это поле представляет собой битовую маску, поэтому и _IOC_READ и _IOC_WRITE могут быть извлечены используя логическую операцию AND.
size Размер передаваемых данных. Емкость этого поля архитектурно-зависима и, сейчас, лежит в диапазоне от 8 до 14 бит. Вы можете определить его значение для вашей архитектуры из макро _IOC_SIZEBITS. Если вы создаете портируемый драйвер, то значение этого поля не должно превышать 255. Использование данного поля не является обязательным. Если вы передаете больший размер данных, то вы можете просто игнорировать его. Скоро, мы увидим пример использования этого поля.
В заголовочном файле <asm/ioctl. h>, который включен в <linux/ioctl. h>, определены следующие макросы, которые помогут установить номер команды:
_IO(type, nr), _IOR(type, nr, dataitem), _IOW(type, nr, dataitem)
Каждое макро соответсвует одному возможному значению для направления передачи.
Поля type и number передаются как аргументы, а поле size вычисляется как sizeof() над аргументом dataitem.
Порядковый номер команд не имеет какого-то специфического значения. Он используется только для различения команд.
27. Операции блокируемого ввода-вывода. Использование очередей.
В реализации отклика на чтение в драйвере может возникнуть проблема, когда, с одной стороны, данные еще не кончились (т. е. нет конца файла), но, в данный момент, еще не готовы.
В этом случае, необходимо отправить вызывающий процесс в спящее состояние.
Когда процесс должен ждать какого-либо события (ожидание данных, или завершение другого процесса), он должен быть погружен в спящее состояние. Это означает, что система приостанавливает исполнение процесса, освобождая процессор для исполнения других процессов. Позже, когда ожидаемое событие реализуется, процесс будет разбужен и продолжит свою работу.
Существует несколько способов управления переводом процесса в спящее состояние и его пробуждения. Каждый из этих способов используется в разных контекстах. Однако, все эти способы используют один и тот же тип данных – очередь ожидания (wait_queue_head_t).
Очередь ожидания – это очередь процессов, ожидающих какого-либо события. Очередь ожидания описывается и инициализируется следующим образом:
wait_queue_head_t my_queue;
init_waitqueue_head (&my_queue);
Даже если очередь ожидания описана статически (static), т. е. она определена не как автоматическая переменная или часть динамически распределенной структуры данных, то, по прежнему, возможна инициализация очереди на этапе компиляции:
DECLARE_WAIT_QUEUE_HEAD (my_queue);
НЕЛЬЗЯ игнорировать процедуру инициализации очереди ожидания. (если не выполнить такую инициализацию - результат работы с очередью будет непредсказуемым).
После инициализации очереди, ее можно использовать для перевода процесса в спящее состояние:
sleep_on(wait_queue_head_t *queue);
Функция выполняет перевод процесса в спящее состояние. Недостатком использования такого способа приостановки выполнения процесса является невозможность его прерывания. Т. е. процесс становится неубиваемым, если ожидаемое им событие не может случиться.
interruptible_sleep_on(wait_queue_head_t *queue);
Прерываемый вариант. Работает также как и sleep_on(), но переведенный в сон процесс может быть прерван сигналом. Разработчики драйверов использовали такую форму вызова достаточно долгое время до появления, описанной ниже, альтернативы wait_event_interruptible().
void wait_event(wait_queue_head_t queue, int condition);
int wait_event_interruptible(wait_queue_head_t queue, int condition);
Эти макросы предоставляют наиболее предпочтительный способ перевода процесса в сон до момента удовлетворения заданного условия. Макросы связывают ожидание события и проверку его возникновения способом, позволяющим избежать проблемы "race condition" (борьба за ресурсы). Код будет спать до момента возникновения условия, которое может быть задано любым логическим выражением языка Си, вычисляемым в true (истина).
Данные макросы расширяются в цикл while, который перевычисляет условие перед каждым повторением тела цикла. Такое поведение отличается от вызова функции или простого макро, где аргументы вычисляются только в момент вызова. Последниее макро реализуется как выражение, которое возвращает ноль в случае успеха, и - ERESTARTSYS, если цикл прерывается сигналом.
Как правило, если одна часть драйвера спит, то существует другая его часть, которая может выполнить пробуждение, при возникновении определенного события. Драйвер пробуждает спящие куски кода в своем обработчике прерываний при получении новой порции данных. Функции для пробуждения процесса:
wake_up(wait_queue_head_t *queue);
Данная функция будит все процессы, которые ожидают данной очереди.
wake_up_interruptible(wait_queue_head_t *queue);
Данная функция будит только те процессы, которые находятся в прерываемом сне. Все остальные процессы, которые заснули по заданной очереди событий, но были переведены в сон с помощью "непрерываемых" функций, останутся в спящем состоянии.
Чтобы быть последовательным, используя interruptible_sleep_on() для перевода в сон, следует использовать wake_up_interruptible() для пробуждения. Пробуждение процесса может быть связано не с тем событием, которого он ожидал. Процес может быть разбужет по другим причинам, например, в результате получения сигнала. Поэтому, спящий код должен проверять необходимое условие после пробуждения.
28. Реализация разграничения доступа на уровне драйвера устройства.
Ограничение доступа по принципу "Один пользователь за раз"
Следующий способ ограничения доступа, который мы сейчас рассмотрим, позволяют одному пользователю открывать устройство много раз из разных процессов, но устройство может быть открыто только одним пользователем за раз. Т. е. до тех пор пока один пользователь использует устройство хотя бы в одном из своих процессов, любой другой пользователь не сможет получить доступ к этому устройству. Такое устройство может быть легко протестировано, так как пользователь может читать и писать данные в устройство одновременно из нескольких процессов. Единственное, нужно понимать, что пользователь несет некоторую ответственность за целостность данных при организации множественного доступа к устройству. Такое управление доступом реализуется добавлением специальной проверки в метод open() драйвера. Понятно, что такая проверка будет выполнена после всех проверок выполняемых файловой системой на основе битов разрешения доступа к файлу устройства. Именно такая политика доступа используется для устройств tty, но реализуется не за счет обращения к внешним привелигированным программам.
Такая политика доступа несколько сложнее в реализации, нежели политика single-open устройства. В этом случае нам необходимо слежение как за счетчиком использования модуля, так и за uid ("user ID" - идентификатор пользователя) владельца ("owner") устройства. Как уже говорилось раньше, лучшим местом для хранения такой информации является структура устройства, однако для минимизации повторяемого кода в наших примерах мы будем использовать для ее хранения глобальную переменную. Обсуждаемое здесь устройство называется sculluid.
Вызов open() разрешает доступ при первом открытии файла, и запоминает владельца устройства. Теперь этот пользователь может открыть данное устройство произвольное количество раз, используя разные процессы для параллельного доступа к этому устройству. В то же самое время, никакой другой пользователь не сможет открыть данное устройство. Вариант метода open() для данного случая во многом похож на тот код, который был приведен ранее, для устройства с политикой single-open, поэтому здесь мы приведем только измененную часть кода, а не весь метод целиком:
spin_lock(&scull_u_lock);
if (scull_u_count &&
(scull_u_owner!= current->uid) && /* allow user */
(scull_u_owner!= current->euid) && /* allow whoever did su */
!capable(CAP_DAC_OVERRIDE)) { /* still allow root */
spin_unlock(&scull_u_lock);
return - EBUSY; /* - EPERM would confuse the user */
}
if (scull_u_count == 0)
scull_u_owner = current->uid; /* grab it */
scull_u_count++;
spin_unlock(&scull_u_lock);
В качестве кода возврата при невыполнении приведенного условия мы используем значение - EBUSY, а не - EPERM. Это подчеркивает тот факт, что пользователь имеет право доступа к устройству, но на данный момент оно занято. "Permition denied" используется, обычно, как ошибка при проверке прав доступа к /dev-файлу устройства, в то время как "Device busy" корректно отображает тот факт, что устройство занято другим пользователем.
Приведенный код проверяет не только идентификатор пользователя (uid), но и эффективный идентификатор пользователя (euid), что позволяет открывать устройство процессам использующим требуемую замену идентификатора пользователя. Проверка возможности перекрытия прав доступа к файлам и каталогам (CAP_DAC_OVERRIDE), дополнительно позволяет ослабить жесткую политику доступа к устройству.
Код метода close() не показан для этого случая, потому что все, что он делает сводится к декрементированию счетчика открытия файла.
29. Отладка модулей ядра с помощью функции printk. Управление кольцевым буфером сообщений ядра.
Наиболее общей техникой отладки является печать. В приложениях пользовательского уровня, такая техника реализуется через вызов функции printf() в определенных местах программы. При отладке кода ядра используется аналогичная функция printk(), доступная в коде ядра.
Одно из отличий функций printk() и printf() заключается в том, что функция printk() позволяет вам классифицировать сообщения согласно, назначенным им, приоритетам. Приоритет можно задать через макроопределение. Например, KERN_INFO один из возможных приоритетов сообщения. Макроопределение приоритета расширяется до строки, которая соединяется со строкой текста сообщения во время компиляции. По этой причине, между указанием приоритета и строкой сообщения не ставится запятая. Приведем два примера вызова функции printk() с отладочным и критическим сообщением.
printk(KERN_DEBUG "Here I am: %s:%i\n", __FILE__, __LINE_&_);
printk(KERN_CRIT "I'm trashed; giving up on %p\n", ptr);
В заголовочном файле <linux/kernel. h> определены восемь возможных уровней приоритета (loglevel).
KERN_EMERG
Используется для “непредвиденных” сообщений, особенно для тех, которые предшествуют краху.
KERN_ALERT
Ситуация, требующая немедленного вмешательства.
KERN_CRIT
Критическая ситуация. Часто связанная с серьезными неисправностями оборудования или программным проблемам.
KERN_ERR
Используется для отчета об условиях возникшей ошибки. Драйвера устройств часто используют KERN_ERR для сообщениях о проблемах, связанных с оборудованием.
KERN_WARNING
Сообщение о проблемных ситуациях, которые сами по себе не создают проблем для системы.
KERN_NOTICE
Ситуация не проблемная, но заслуживает внимания. Часто используется в сообщениях системы безопасности.
KERN_INFO
Информационное сообщение. Используется в сообщениях о найденном оборудовании при загрузке драйверов.
KERN_DEBUG
Используется для отладочных сообщений.
Каждая строка (при подстановке макроопределения) представляет целое число в угловых скобках. Диапазон изменения целого числа от 0 до 7. Причем, чем меньше значение тем выше приоритет. В зависимости от уровня приоритета, ядро может выводить сообщения на текущую консоль (текстовый терминал), на принтер или в файл.
Функция printk() пишет сообщение в круговой буфер длиной LOG_BUF_LEN байт, определенный в kernel/printk. c. Затем, просыпается один из процессов ожидающий сообщение. Т. е. это либо процесс, который спит в системном вызове syslog, либо процесс, который читает /proc/kmsg. Эти два интерфейса к log-машине практически эквивалентны, но чтение из /proc/kmsg “съедает” данные из log-буфера, в то время как системный вызов syslog может опционально вернуть данные в буфер пока они нужны другим процессам. В общем, чтение файла из /proc проще, поэтому оно используется в klogd по умолчанию.
После остановки klogd видно, что файл в /proc организован в виде очереди FIFO. Причем, читающий процесс блокируется для ожидания следующих данных. Таким образом, вы не можете прочитать сообщения этим способом, если klogd уже занят чтением буфера, потому что, иначе, это приведет к проблеме конкуренции доступа к данным. Если круговой буфер переполняется, то функция printk начинает затирать наиболее старые данные буфера. Таким образом, старые данные теряются. Эта проблема не существенна, по сравнению с преимуществами организации кругового буфера. Например, круговой буфер позволяет системе работать даже, если нет процесса, обрабатывающего этот буфер. Другом преимуществом Linux подхода к реализации этого механизма заключается в том, что функция printk() может быть вызвана откуда угодно, даже из обработчика прерываний, без ограничений на количество выводимых данных. Единственным недостатком является возможность потери данных.
Если процесс klogd запущен, то он извлекает из буфера сообщения ядра и диспетчеризует их согласно настройкам в файле /etc/syslog. conf. syslogd различает события согласно их принадлежности и приоритету. Если klogd не запущен, то данные остаются в круговом буфере, либо пока их кто-нибудь не прочитает, либо пока буфер не переполнится.
30. Выделение и освобождение памяти в режиме ядра. Особенности и отличие от пользовательского режима.
Управление динамической памятью в ядре не имеет принципиальных отличий. Программа может получить память используя функцию kmalloc() и освободить ее, с помощью kfree().Эти функции очень похожи на malloc() и free(), за тем исключением, что в функцию kmalloc() передается дополнительный аргумент – приоритет. Обычно приоритет принимает значения GFP_KERNEL или GFP_USER. GFP представляет собой акроним от “get free page” - взять свободную страницу.
void *kmalloc(unsigned int size, int priority);
void kfree(void *obj);
Аналоги функций malloc() и free() в пространстве ядра. Используют значение GFP_KERNEL для указания приоритета.
Функция kmalloc() и связанная с ней система управления памятью представляет собой мощный и достаточно простой инструмент распределения необходимого пространства памяти. Вызов kmalloc() высокопроизводителен если не оперирует с блоками. Функция не очищает распределенную память - в полученном куске остается предыдущий контент. Распределенный регион непрерывен в физической памяти.
Наиболее часто используемым флагом является флаг GFP_KERNEL, означающий, что распределение выполняется в интересах процесса запущенного в пространстве ядра. Использование GFP_KERNEL означает, что kmalloc() может перевести текущий процесс в спящее состояние на время ожидания требуемой страницы памяти при работе с нижней памятью (low memory situations). Во время спячки процесса, ядро выполняет соответствующие действия для получения страницы памяти, т. е. либо сбрасывает дисковые буфера, либо реализует своппинг памяти из пользовательского процесса. Все флаги определены в заголовочном файле <linux/mm. h>: индивидуальные флаги префексированы двойным знаком подчеркивания, как __GFP_DMA; флаги, которые могут использоваться в сочетаниях не имеют такого префикса и иногда называются приоритетами распределения.
GFP_KERNEL
Обычное распределение памяти в ядре. Может привести процесс в спящее состояние.
GFP_BUFFER
Используется для управления буфером кэша. Этот приоритет позволяет перевести процесс в сон. Он отличается от GFP_KERNEL тем, что освобождение памяти будет производится сбросом на диск грязных страниц (dirty pages). Назначение флага заключается в избежании ситуации дэдлока (deadlock - взаимной блокировки) в случае, если подсистеме ввода/вывода самой потребуется память.
GFP_USER
Используется для распределения памяти для пользовательского процесса. Может перевести процесс в спящее состояние. Имеет низкий приоритет.
31. Файловая система /proc. Создание файлов для чтения и файлов чтения/записи.
Файловая система /proc представляет собой специальную, программно-реализованную файловую систему, которая связана с функциями ядра, которые генерируют содержание файла на лету, во время чтения файла. (например, таким как /proc/modules, который возвращает список загруженных, в данный момент, модулей).
Драйвера устройств также используют /proc для передачи информации в пространство пользователя. Файловая система /proc является динамической системой, и ваш модуль может добавлять и удалять файловый элемент из этой системы во время своей работы. Все модули, которые работают с файловой системой /proc должны включать заголовочный файл <linux/proc_fs. h>, где определены соответствующие функции.
Для создания файла в файловой системе /proc, который будет доступен только для чтения, драйвер должен реализовать функцию, которая будет наполнять файл содержанием во время его чтения. Когда некоторый процесс читает файл (используя системный вызов read()), запрос достигает вашего модуля через один из двух возможных интерфейсов, в зависимости от способа регистрации файла. Рассмотрим интерфейсы чтения.
В обоих случаях, ядро распределяет страницу памяти, размером PAGE_SIZE байт, куда драйвер будет писать данные, передаваемые в пользовательское пространство. Рекомендуемым интерфейсом является интерфейс read_proc, но, также, существует старый интерфейс, имеющий название get_info.
int (*read_proc)(char *page, char **start, off_t offset, int count, int *eof, void *data);
Параметр page представляет собой указатель на буфер, куда вы пишите свои данные. Параметр start используется функцией для указания размещения данных на странице. Параметры offset и count (размер буфера) имеют то же значение, что и в реализации метода read(). Аргумент eof указывает на целое число, которое должно быть установлено драйвером, для сообщения о том, что данные закончились. Параметр data представляет собой драйверо-зависимый указатель, который может быть использован для внутренних целей разработчика драйвера.
int (*get_info)(char *page, char **start, off_t offset, int count);
get_info представляет собой старый интерфейс, используемый при чтении из файла файловой системы /proc. Все аргументы имеют те же самые значения, что и для интерфейса read_proc. Недостатком этого интерфейса является отсутствие указателя-индикатора eof (“end-of-file”) и указателя data.
Обе функции должны возвращать количество байт данных действительно размещенных в буфере страницы. Другими возвращающими параметрами являются параметры *eof и *start. Параметр eof это простой флаг окончания данных. Более сложным является параметр start.
Главной проблемой обычной реализации пользовательского дополнения в файловую систему /proc, является использование одной страницы памяти для передачи данных. Это ограничивает размер файла четырьмя килобайтами (зависит от аппаратной платформы). Аргумент start нужен для поддержки больших файлов данных, и может быть проигнорирован.
Если функция proc_read() не устанавливает указатель *start (т. е. start равен NULL), то ядро предполагает, что параметр offset был проигнорирован, и что страница данных целиком занята файлом, который вы хотите возвратить в пространство пользователя. Если нужно передать файл большего размера, состоящий из кусков, то можно установить *start равным page, чтобы вызывающая программа знала, что новые данные расположены начиная с начала буфера. Конечно, надо пропустить первые offset байт данных, которые уже были возвращены в предыдущем вызове.
После реализации функции read_proc() необходимо зарегистрировать элемент в иерархии файловой системы /proc. Возможны два способа такой регистрации, в зависимости от версии ядра, в котором вы собираетесь использовать драйвер. Простейший способ заключается в вызове функции create_proc_read_entry(). Приведем пример регистрации элемента /proc/scullmem для драйвера scull.
create_proc_read_entry("scullmem", - имя файла в /proc
0 /* default mode */, - права доступа к файлу (0 всеобщий на чтение),
NULL /* parent dir */, - указатель на родительский каталог для файла
scull_read_procmem, - указатель на функцию read_proc()
NULL /* client data */); - указатель на данные, которые будут переданы
При выгрузке модуля, его элементы в /proc должны быть удалены. Функция remove_proc_entry() используется для удаления элементов созданных вызовом create_proc_read_entry().
remove_proc_entry("scullmem", NULL /* parent dir */);
Для создания файла в файловой системе /proc, который будет доступен для записи/чтения надо организовать передачу данных модулю ядра посредством файловой системы /proc.
Поскольку файловая система /proc была написана, главным образом, для того чтобы получать данные от ядра, она не предусматривает специальных средств для записи данных в файлы. Структура proc_dir_entry не содержит указатель на функцию-обработчик записи. Поэтому, вместо того, чтобы писать в /proc напрямую, мы вынуждены будем использовать стандартный для файловой системы механизм.
Linux предусматривает возможность регистрации файловой системы. Так как каждая файловая система должна иметь собственные функции, для обработки inode и выполнять файловые операции, то имеется специальная структура, которая хранит указатели на все необходимые функции-обработчики — struct inode_operations, которая включает указатель на struct file_operations. Файловая система /proc, всякий раз, когда мы регистрируем новый файл, позволяет указать — какая struct inode_operations будет использоваться для доступа к нему. В свою очередь, в этой структуре имеется указатель struct file_operations, а в ней уже находятся указатели на наши функции-обработчики.
Обратите внимание: стандартные понятия "чтение" и "запись", в ядре имеют противоположный смысл. Функции чтения используются для записи в файл, в то время как функции записи используются для чтения из файла. Причина в том, что понятия "чтение" и "запись" рассматриваются здесь с точки зрения пользователя: если процесс читает что-то из ядра — ядро должно записать эти данные, если процесс пишет — ядро должно прочитать то, что записано.
Еще один интересный момент — функция module_permission. Она вызывается всякий раз, когда процесс пытается обратиться к файлу в файловой системе /proc, и принимает решение — разрешить доступ к файлу или нет. На сегодняшний день, решение принимается только на основе выполняемой операции и UID процесса, но в принципе возможна и иная организация принятия решения, например, разрешать ли одновременный доступ к файлу нескольким процессам и пр..
Причина, по которой для копирования данных используются функции put_user и get_user, состоит в том, что процессы в Linux (по крайней мере в архитектуре Intel) исполняются в изолированных адресных пространствах, не пересекающихся с адресным пространством ядра. Это означает, что указатель, не содержит уникальный адрес физической памяти — он хранит логический адрес в адресном пространстве процесса.
Единственное адресное пространство, доступное процессу — это его собственное адресное пространство. Практически любой модуль ядра, должен иметь возможность обмена информацией с пользовательскими процессами. Однако, когда модуль ядра получает указатель на некий буфер, то адрес этого буфера находится в адресном пространстве процесса. Макрокоманды put_user и get_user позволяют обращаться к памяти процесса по указанному им адресу.
/* Для передачи данных из пространства ядра в пространство пользователя
* следует использовать put_user. В обратном направлении — get_user.
* Эта функция принимает решение о праве на выполнение операций с файлом
* 0 — разрешено, ненулевое значение — запрещено.
* Операции с файлом: 2-Запись(user ->модуль ядра) и 4-Чтение (модуль ядра -> user) 0-Х
* Эта функция проверяет права доступа к файлу */
static int module_permission(struct inode *inode, int op, struct nameidata *foo)
{ /* Позволим любому читать файл, но писать — только root-у (uid 0) */
if (op == 4 || (op == 2 && current->euid == 0))
return 0;
return - EACCES; /* Если что-то иное — запретить доступ */ }
static struct inode_operations Inode_Ops_4_Our_Proc_File =
{ .permission = module_permission, /* проверка прав доступа */ };
32. Таймеры ядра. Инициализация таймера, его использование и удаление.
Таймеры используются для планирования исполнения функции (обработчика таймера) в заданное время. Таким образом, их работа отличается тем, что можно определить время запуска обработчика таймера. С другой стороны, таймеры ядра похожи на очереди задач в том, что функция зарегистрированная в таймере ядра выполняется только один раз.
Иногда возникает необходимость выполнения операций отсоединенных от какого-либо контекста процесса. Например, выключение мотора флоппи-дисковода, или завершение других долгозавершаемых операций. В этом случае, задержка на ожидание окончания завершения таких операций не должна сказываться на работе приложений. Использование, для этих целей, очереди задач было было бы расточительно, потому что задача, запускаемая из очереди, должна перерегистрировать себя в очереди до тех пор, пока операция не будет завершена.
Таймер много проще в использовании. Вы регистрируете свою функцию единожды, и ядро вызывает ее при истечении счета таймера. Данная функциональность часто используется в самом ядре, но, иногда, требуется и в драйверах, как например, в случае управления мотором флоппи-дисковода.
Таймеры ядра организуются в двунаправленный связанный список. Это означает, что вы можете создавать столько таймеров, сколько захотите. Таймер характеризуется значением таймаута (в джиффисах), и функцией, вызываемой при истечении таймера. Обработчик таймера принимает аргумент, который сохраняется в структуре данных, вместе с указателем на сам обработчик.
Структура таймера определена в заголовочном файле <linux/timer. h> и выглядит следующим образом:
struct timer_list {
struct timer_list *next; /* never touch this*/
struct timer_list *prev; /* never touch this*/
unsigned long expires; /* значение таймаута в джиффисах*/
unsigned long data; /* аргумент для обработчика*/
void (*function)(unsigned long); /* handler of the timeout (обработчик)*/
volatile int running; /* added in 2.4; don't */
};
Значение таймера задается в джиффисах. Таким образом, timer->function() будет запущена тогда, когда значение джиффисов будет больше или равно заданному значению timer->expires. Таймаут задается в абсолютных значениях джиффисов, обычно получаемых прибавлением желаемой задержки к текущему значению.
Как только структура timer_list инициализируется, add_timer() вставляет ее в сортированный список, который проверяется около 100 раз в секунду. Увеличение частоты системного таймера не увеличит частоту обработки этого списка. Для работы с таймером используются следующие функции:
void init_timer(struct timer_list *timer);
Эта inline-функция используется для инициализации структуры таймера. В текущей реализации, она обнуляет указатели prev и next (и флаг running на системах SMP). Для совместимости со следующими версиями ядра, программистам крайне рекомендуется использовать эту функцию для инициализации таймера, и не использовать явную инициализацию указателей этой структуры.
void add_timer(struct timer_list *timer);
Данная функция вставляет таймер в глобальный список активных таймеров.
int mod_timer(struct timer_list *timer, unsigned long expires);
Для изменения времени истечения таймера, который уже находится в глобальном списке активных таймеров. После ее вызова, для данного таймера будет использовано новое значение таймаута.
int del_timer(struct timer_list *timer);
Функция del_timer() удаляет таймер из списка активных таймеров (до его истечения). Если таймер был действительно установлен в очередь, то del_timer() возвратит 1, иначе - 0. (при истечении таймера, он будет удален из списка автоматически)
При использовании таймера следует иметь ввиду, что таймер истекает в точно назначенное время, даже если процессор выполняет, в этот момент, системный вызов. Раньше мы предполагали, что когда процесс выполняется в пространстве ядра, то он не планируется вообще, т. е. не может быть прерван. Однако, таймеры являются специальным случаем, и выполняют все свои задачи независимо от текущего процесса. И хотя система может оказаться жестко заблокированной системным вызовом находящимся в ожидании, но и очередь таймера, и таймеры ядра будут продолжать обрабатываться.
33. Использование средств tasklet. Особенности и отличие от таймеров ядра.
Практически перед выпуском ядра 2.4, разработчики добавили новый механизм для запуска отложенных задач ядра. Этот механизм, называемый такслетами, представляет сейчас наиболее предпочтительный способ исполнения нижних половинок (botton-half). Кроме того, в данный момент, нижние половинки, сами по себе, реализованы через такслеты.
Во многом, тасклеты похожи на очереди задач. Они предоставляют способ безопасного запуска отложенных задач, и всегда исполняются в режиме прерывания. Как и очереди задач, такслеты могут быть запущены только единожды, даже при многократной установке в планировщик, но тасклеты могут быть запущены параллельно с другими тасклетами на системах SMP. Также, на системах SMP существует гарантия, что тасклеты будут запущены на том CPU, который впервые планировал их, что обеспечивает лучшее использование кэша и более высокую производительность.
Каждый тасклет имеет связанную с ним функцию, которыя вызывается тогда, когда тасклет должен быть выполнен. Жизнь некоторых разработчиков ядра была бы проще, если бы эта функция принимала бы свой единственный аргумент как экземпляр типа unsigned long. С другой стороны, в некоторых случаях, предпочтительнее использовать в качестве аргумента указатель. Как бы там не было, но проблемой это не является, так как преобразование аргумента long в тип указателя является совершенно безопасным и может быть использовано на всех поддерживаемых платформах. Мы остановимся на этом вопросе в главе 13 "mmap и DMA". Обсуждаемая тасклет-функция не возвращает значения (т. е. возвращает void). Поддержка тасклетов предоставляется заголовочным файлом <linux/interrupt. h>. Тасклет может быть объявлен с помощью одного из следующих макросов:
DECLARE_TASKLET(name, function, data);
Объявляет тасклет с именем name. При исполнении тасклета вызывается функция function, в которую передается значение data типа unsigned long.
DECLARE_TASKLET_DISABLED(name, function, data);
Так же как и в предыдущем случае объявляет тасклет, но с начальным состоянием "disabled" (запрещено), означающем, что он может учавствовать в планировке задач, но не может быть исполнен до тех пор, пока не будет разрешен в последствии.
Пример драйвера jiq, при компиляции для ядра 2.4 реализует поддержку файла /proc/jiqtasklet, который работает также, как и другие файлы, обрабатываемые драйвером jiq, но использует тасклеты. Мы не эмулируем тасклеты для старых версий ядра в нашем заголовочном файле sysdep. h. Модуль объявляет свой тасклет следующим образом:
void jiq_print_tasklet (unsigned long);
DECLARE_TASKLET (jiq_tasklet, jiq_print_tasklet, (unsigned long) &jiq_data);
Когда драйвер хочет диспетчеризовать тасклет для запуска, он вызывает функцию tasklet_schedule():
tasklet_schedule(&jiq_tasklet);
Если тасклет разрешен, то как только он диспетчеризуется, он будет запущен сразу же как только это будет возможно из соображений безопасности. Тасклеты могут перепланировать сами себя много раз, также как и очереди задач. Тасклет не должен беспокоиться о запуске своей копии на многопроцессорной системе, так как в ядре предприняты шаги к тому, чтобы гарантировать запуск каждого из тасклетов только в одном месте. Также, необходимо понимать, что если ваш драйвер реализует множество тасклетов, то он должен быть готов к тому, что более чем один из них может исполняться одновременно. В этом случае, для защиты критических секций кода должен быть использован spinlock. Так как семафоры могут уйти в состояние сна, то они не могут быть использованы в тасклетах, исполняемых в режиме прерывания.
Типичное содержимое файла /proc/jiqtasklet: time delta interrupt pid cpu command
45472head
Тасклет всегда запускается на одном и том же CPU, даже если вывод файла производится на двупроцессорной системе.
void tasklet_disable(struct tasklet_struct *t);
Функция запрещает данный тасклет. Тасклет может обрабатываться планировщиком по tasklet_schedule(), но его исполнение будет отложено до тех пор, пока тасклет не будет разрешен.
void tasklet_enable(struct tasklet_struct *t);
Разрешает тасклет, который был предварительно запрещен. При этом, если тасклет был уже спланирован, то он будет скоро запущен, но не прямо из tasklet_enable().
void tasklet_kill(struct tasklet_struct *t);
Эта функция может быть использована для тасклетов, которые перепланировали сами себя неконтролируемое число раз. Функция tasklet_kill() удалит тасклеты из любой очереди, в которой он содержится. Для того, чтобы избежать гонки (race condition) в запущенных, и планирующих себя тасклетах, эта функция ожидает завершения работы тасклета, и только потом извлекает его из очереди. Таким образом, вы можете быть уверены, что тасклеты не будут прерваны во время исполнения. Однако, если тасклет не является запущенным, и не перепланирует сам себя, то функция tasklet_kill() может повиснуть. tasklet_kill() не может быть вызвана во время прерывания.
34. Механизм очередей Workqueue.
Необходимый заголовочный файл
#include <linux/workqueue. h> /* очереди задач */
#include <linux/sched. h> /* Взаимодействие с планировщиком */
#include <linux/init. h> /* макросы __init и __exit */
Некоторые функции, относящиеся к work_queue доступны только если модуль лицензирован под GPL - необходимо включить MODULE_LICENSE("GPL");
Очередь задач, создается для того, чтобы поместить в очередь таймера (workqueue. h)
static struct workqueue_struct *my_workqueue;
Функция инициализации
int __init init_module()
{
…;
Создать очередь задач с нашей задачей и поместить ее в очередь таймера
my_workqueue = create_workqueue(MY_WORK_QUEUE_NAME);
queue_delayed_work(my_workqueue, &Task, 100);
…
}
Завершение работы
void __exit cleanup_module()
{
…
flush_workqueue(my_workqueue); /* ждать пока отработает таймер */
destroy_workqueue(my_workqueue);
…
}
35. Обработка прерываний. Установка и удаление обработчика прерываний.
В Linux аппаратные прерывания называются IRQ (сокращенно от Interrupt ReQuests — Запросы на Прерывание). Имеется два типа IRQ: "короткие" и "длинные". "Короткие" IRQ занимают очень короткий период времени, в течение которого работа операционной системы будет заблокирована, а так же будет невозможна обработка других прерываний. "Длинные" IRQ могут занять довольно продолжительное время, в течение которого могут обрабатываться и другие прерывания (но не прерывания из того же самого устройства). Поэтому, иногда бывает благоразумным разбить выполнение работы на исполняемую внутри обработчика прерываний (т. е. подтверждение прерывания, изменение состояния и пр.) и работу, которая может быть отложена на некоторое время (например постобработка данных, активизация процессов, ожидающих эти данные и т. п.). Если это возможно, лучше объявлять обработчики прерывания "длинными".
Тебуется заголовочный файл #include <linux/interrupt. h> и <linux/sched. h>:
IRQ нумеруются и каждое аппаратное устройство в системе связывается с номером IRQ. Связывание номера IRQ с устройством позволяет центральному процессору выяснить, какое устройство сгенерировало каждое прерывание, и, следовательно, позволяет ему выполнить переход к нужному обработчику прерывания.
int request_irq(unsigned int irq,
void (*handler)(int, void *, struct pt_regs *),
unsigned long flags,
const char *dev_name,
void *dev_id);
Устанавливается обработчик прерывания вызовом функции request_irq(). Ей передаются номер IRQ, имя функции-обработчика, флаги, имя для /proc/interrupts и дополнительный параметр для обработчика прерываний. Флаги могут включать SA_SHIRQ, чтобы указать, что прерывание может обслуживаться несколькими обработчиками (обычно, по той простой причине, что на одном IRQ может "сидеть" несколько устройств) и SA_INTERRUPT, чтобы указать, что это "короткое" прерывание. Эта функция установит обработчик только в том случае, если на заданном IRQ еще нет обработчика прерывания, или если существующий обработчик зарегистрировал совместную обработку прерывания флагом SA_SHIRQ.
Обработка прерываний.
// Захват прерывания
if (request_irq(IRQ_NUM, irq_handler, 0, DEV_NAME, NULL)){
printk("Kernel: IRQ allocation failed\n");
release_mem_region(MEM_START, MEM_COUNT);
release_region(PORT_START, PORT_COUNT);
return - EBUSY;
}
Удаляется обработчик прерывания вызовом функции free_irq()
void free_irq(unsigned int irq, void *dev_id);
36. Назначение линии IRQ обработчику прерывания.
Устанавливается обработчик прерывания вызовом функции request_irq. Ей передаются номер IRQ, имя функции-обработчика, флаги, имя для /proc/interrupts и дополнительный параметр для обработчика прерываний. Флаги могут включать SA_SHIRQ, чтобы указать, что прерывание может обслуживаться несколькими обработчиками (обычно, по той простой причине, что на одном IRQ может "сидеть" несколько устройств) и SA_INTERRUPT, чтобы указать, что это "короткое" прерывание. Эта функция установит обработчик только в том случае, если на заданном IRQ еще нет обработчика прерывания, или если существующий обработчик зарегистрировал совместную обработку прерывания флагом SA_SHIRQ.
37. Механизм обработки прерываний в ОС LINUX.
1) прерывание, - выбор нужного обработчика (можно обрабатывать сразу или позже)
2) назначение приоритета
3) выбор обработчика
Применяемый в ОС механизм обработки внутренних и внешних прерываний в основном зависит от того, какая аппаратная поддержка обработки прерываний обеспечивается конкретной аппаратной платформой. Но существует соглашение о базовых механизмах прерываний.
Суть механизма состоит в том, что каждому возможному прерыванию процессора (будь то внутреннее или внешнее прерывание) соответствует некоторый фиксированный адрес физической оперативной памяти. В тот момент, когда процессору разрешается прерваться по причине наличия внутренней или внешней заявки на прерывание, происходит аппаратная передача управления на ячейку физической оперативной памяти с соответствующим адресом - обычно адрес этой ячейки называется "вектором прерывания" (как правило, заявки на внутреннее прерывание, т. е. заявки, поступающие непосредственно от процессора, удовлетворяются немедленно).
Дело ОС - разместить в соответствующих ячейках оперативной памяти программный код, обеспечивающий начальную обработку прерывания и инициирующий полную обработку.
В векторе прерывания, соответствующем внешнему прерыванию, т. е. прерыванию от некоторого внешнего устройства, содержатся команды, устанавливающие уровень выполнения процессора (уровень выполнения определяет, на какие внешние прерывания процессор должен реагировать незамедлительно) и осуществляющие переход на программу полной обработки прерывания в соответствующем драйвере устройства. Для внутреннего прерывания (например, прерывания по инициативе программы пользователя при отсутствии в основной памяти нужной страницы виртуальной памяти, при возникновении исключительной ситуации в программе пользователя и т. д.) или прерывания от таймера в векторе прерывания содержится переход на соответствующую программу ядра ОС
38. Верхняя и нижняя половины обработчика прерываний.
Иногда бывает благоразумным разбить выполнение работы на исполняемую внутри обработчика прерываний (т. е. подтверждение прерывания, изменение состояния и пр.) и работу, которая может быть отложена на некоторое время (например постобработка данных, активизация процессов, ожидающих эти данные и т. п.).
Bottom halves - это самый старый механизм отложенного исполнения задач ядра и был доступен еще в Linux 1.x.. В Linux 2.0 появился новый механизм - "очереди задач" ('task queues').
Bottom halves упорядочиваются блокировкой (spinlock) global_bh_lock, т. е. только один bottom half может быть запущен на любом CPU за раз. Однако, если при попытке запустить обработчик, global_bh_lock оказывается недоступна, то bottom half планируется на исполнение планировщиком - таким образом обработка может быть продолжена вместо того, чтобы стоять в цикле ожидания на global_bh_lock.
Всего может быть зарегистрировано только 32 bottom halves. Функции для работы с ними экспортируются в модули.
Bottom halves, по сути своей, являются глобальными "блокированными" тасклетами (tasklets), так, вопрос: "Когда исполняются обработчики bottom half ?", в действительности должен звучать как:
"Когда исполняются тасклеты?". На этот вопрос имеется два ответа:
а) при каждом вызове schedule()
б) каждый раз, при исполнении кода возврата из прерываний/системных вызовов (interrupt/syscall return path) в entry. S.
|
Из за большого объема этот материал размещен на нескольких страницах:
1 2 3 |


