Содержание


Обслуживание периферии в коде модулей ядра: Часть 62. Работа с драйверами в пользовательском пространстве

Comments

В предыдущей статье мы практически завершили обсуждение широкого круга вопросов, связанных с поддержкой периферийного оборудования из кода модуля ядра. Но остался последний вопрос, который следует рассмотреть для создания законченной картины. В этой статье, завершающей данный мини цикл, мы изучим выполнение операций с внешними устройствами из процессов пользовательского пространства, которые не требуют наличия привилегий супервизора от выполняющегося кода.

Операции I/O в пространстве пользователя

Ряд функций управления реальным устройством можно выполнять в пространстве пользователя, не прибегая к технике написания модулей и не входя в адресное пространство ядра. Например, работа в пространстве пользователя может иметь смысл, когда вы начинаете использовать новое или необычное оборудование. Организовав работу таким способом, можно научиться управлять незнакомым оборудованием без риска подвешивания системы в целом. А уже после этого выделение написанного программного обеспечения в модуль ядра должно пройти без особых сложностей.

Может сложиться впечатление, что, выполняя обмен данными из пространства пользователя, можно создать только простейшие учебные приложения. Но это мнение опровергается наличием известных публичных проектов, использующих ввод-вывод только из пространства пользователя, например:

  • безальтернативная графическая подсистема X11, встречающаяся на всех UNIX системах. Базовый вариант этого пакета, не расширенный проприетарными видеодрайверами пространства ядра, не нуждается в модулях ядра, так как работа с портами и видеопамятью адаптеров успешно осуществляется из пространства пользователя, а аппаратные прерывания, которые могут генерироваться оборудованием (обратный ход кадра) система X11 не использует. Именно эта особенность архитектуры базовой системы X11 обеспечила возможность её лёгкого переноса в самые экзотические системы UNIX (как например MINIX 3) и повсеместное применение в мире UNIX.
  • Проект libusb — предоставление API для поддержки всего спектра возможных USB-устройств из пространства пользователя. Здесь смысл и возможности реализации основаны на другой концепции, где низкоуровневый обмен с USB-устройством (требующий обработки прерываний и других возможностей ядра) обеспечивается базовым уровнем поддержки USB, встроенным в ядро. Сам проект libusb является надстройкой пользовательского пространства, обращающейся к базовым возможностям посредством системных вызовов. Показателем признания качества libusb можно считать, что этот API используют, в свою очередь, другие крупные проекты, например, CUPS (Common Unix Printing System — основная на сегодня подсистема печати в Linux) и др.

Используемый инструментарий

Начнём с изучения портов, реально присутствующих в системе, которые мы сможем использовать для обмена:

$ cat /proc/ioports
0000-0cf7 : PCI Bus 0000:00
  0000-001f : dma1
  0020-0021 : pic1
  0040-0043 : timer0
  0050-0053 : timer1
  0060-0060 : keyboard
  0062-0062 : EC data
  0064-0064 : keyboard
  0066-0066 : EC cmd
  0070-0071 : rtc0
...
  0378-037a : parport0
...
  03f8-03ff : serial

Ввод-вывод можно реализовать несколькими способами. Первый из которых предполагает использование функций ввода-вывода, объявленных в файле <sys/io.h>:

unsigned char inb (unsigned short int port);
unsigned short int inw (unsigned short int port);
unsigned int inl (unsigned short int port);
void outb (unsigned char value, unsigned short int port);
void outw (unsigned short int value, unsigned short int port);
void outl (unsigned int value, unsigned short int port);

Сразу же акцентируем внимание на том, что при внешней полной идентичности с аналогичными функциями ядра, эти вызовы представляют собой совершенно другие реализации. Функции ядра размещаются в коде ядра, а пользовательские функции реализуются инлайновыми ассемблерными вставками GCC (как это обсуждалось в предыдущих статьях):

static __inline unsigned char inb (unsigned short int __port) {
   unsigned char _v;
   __asm__ __volatile__ ("inb %w1,%0":"=a" (_v):"Nd" (__port));
   return _v;
}

Из за такого инлайнового способа определения, при компиляции GCC должны быть обязательно включены опции оптимизации (как минимум -O или -O2).

Но непривилегированным процессам запрещены операции обращения к портам ввода-вывода. К таким процессам (не имеющим достаточных привилегий) относятся процессы, выполняющихся в кольце защиты 3 или запущенные не от имени пользователя root (позже будет приведён пример). Для разрешения обменных операций следует:

  1. Отобразить диапазон портов ввода-вывода в пространство задачи и разрешить их использовать:
    int ioperm (unsigned long int from, unsigned long int num, int turn_on);

    Вызов ioperm() устанавливает биты привилегий для доступа к области портов ввода/вывода, где:

    • from— начальный номер порта области;
    • num— число портов в области;
    • turn_on — разрешить (1) или запретить (0) привилегированные операции.

    Таким образом можно изменить привилегии только для первых 0x3ff портов ввода-вывода, а если нужно получить тот же результат для всех 65536 портов, то можно воспользоваться системным вызовом iopl().

  2. Изменить уровень привилегированности пользовательского процесса:
    int iopl (int level);

    Вызов iopl() меняет уровень привилегий ввода-вывода, так всем процессам, уровень кольца защиты которых ниже или равен (более привилегированные) значению level, будут разрешены привилегированные операции. Эти права наследуются через fork() и execve(). В дополнение к неограниченному доступу к портам ввода-вывода работа на высоком уровне привилегий также позволяет процессу отключать прерывания. Но это довольно небезопасно, так как может привести к сбою системы.

Естественно, что для получения привилегий ввода-вывода любым из способов процесс должен обладать правами root. После получения привилегий процесс может выполнять вызовы out*() и in*().

Другой способ основывается на том, что Linux отображает пространство аппаратных портов в файл:

$ ls -l /dev/port
crw-r----- 1 root kmem 1, 4 нояб. 23 14:31 /dev/port

Чтобы воспользоваться этой возможностью следует:

  1. открыть fd = open() указанный файл (естественно, с правами root);
  2. сдвинуть указатель позиции в файле на требуемый порт: lseek( fd, port, SEEK_SET );
  3. считать read( fd, &data, 1 ) или записать write( fd, &data, 1 ) данные из порта или в порт.

Смысл проделываемых операций состоит в том, что мы поручаем ядру Linux выполнить для нас операции ввода-вывода. Хотя этот способ будет намного медленнее, но он не требует ни получения дополнительных привилегий (что небезопасно), ни оптимизации при компиляции (что иногда нежелательно).

Помимо возможности ввода/вывода, для таких программ, как правило, нужно предотвратить выгрузку страниц программы на диск:

#include <sys/mman.h>
int mlock( const void *addr, size_t len );
int munlock( const void *addr, size_t len );
int mlockall( int flags );
int munlockall( void );

Наибольшее значение имеет вызов mlockall(), параметр flags которого может быть равен:

  • MCL_CURRENT— локировать все страницы, которые на текущий момент отображены в адресное пространство процесса;
  • MCL_FUTURE— локировать все страницы, которые будут отображаться в будущем в адресное пространство процесса.

В листинге 1 представлен пример кода, демонстрирующего данные концепции. Этот пример также можно найти в архиве user_io.tgz в разделе "Материалы для скачивания".

Листинг 1. Операции ввода-вывода в пространстве пользователя.
#include <stdio.h>
#include <unistd.h>
#include <sys/io.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>

#define PARPORT_BASE 0x378

void do_io( unsigned long addr ) {
   unsigned char zero = 0, readout = 0;
   printf( "\twriting: 0x%02x to 0x%lx\n", zero, addr );
   outb( zero, addr );
   usleep( 1000 );
   readout = inb( addr + 1 );
   printf( "\treading: 0x%02x from 0x%lx\n", readout, addr + 1 );
}

void do_read_devport( unsigned long addr ) {
   unsigned char zero = 0, readout = 0;
   int fd;
   printf( "/dev/port :\n" );
   if( ( fd = open( "/dev/port", O_RDWR ) ) < 0 ) {
      perror( "reading /dev/port method failed" ); return;
   }
   if( addr != lseek( fd, addr, SEEK_SET ) ) {
      perror( "lseek failed" ); close( fd ); return;
   }
   printf( "\twriting: 0x%02x to 0x%lx\n", zero, addr );
   write( fd, &zero, 1 );
   usleep( 1000 );
   read( fd, &readout, 1 );
   printf( "\treading: 0x%02x from 0x%lx\n", readout, addr + 1 );
   close( fd );
   return;
}

void do_ioperm( unsigned long addr ) {
   printf( "ioperm :\n" );
   if( ioperm( addr, 2, 1 ) ) {
      perror( "ioperm failed" ); return;
   }
   do_io( addr );
   if( ioperm( addr, 2, 0 ) ) perror( "ioperm failed" );
   return;
}

int iopl_level = 3;
void do_iopl( unsigned long addr ) {
   printf( "iopl :\n" );
   if( iopl( iopl_level ) ) {
      perror( "iopl failed" ); return;
   }
   do_io( addr );
   if( iopl( 0 ) ) perror( "ioperm failed" );
}

int main( int argc, char *argv[] ) {
   unsigned long addr = PARPORT_BASE;
   if( argc > 1 )
      if( !sscanf( argv[ 1 ],"%lx", &addr ) ) {
         printf( "illegal address: %s\n", argv[ 1 ] );
         return EXIT_FAILURE;
      };
   if( argc > 2 ) iopl_level = atoi( argv[ 2 ] );
   do_read_devport( addr );
   do_ioperm( addr );
   do_iopl( addr );
   return errno;
}

Запустим пример и изучим результаты его работы:

$ sudo ./ioports
/dev/port :
        writing: 0x00 to 0x378
        reading: 0x78 from 0x379
ioperm :
        writing: 0x00 to 0x378
        reading: 0x78 from 0x379
iopl :
        writing: 0x00 to 0x378
        reading: 0x78 from 0x379

Особенности ввода/вывода в пространстве пользователя

Если попытаться выполнять обменные операции, не получив предварительно привилегий с помощью вызова ioperm() или iopl(), то программа немедленно прервётся на этом операторе. Также странным образом отличается выполнение примера с правами root и без них (в архиве user_io.tgz представлен пример ioperm.c, выполняющий обмен данными без получения привилегий):

$ ./ioperm
writing: 0x00 to 0x378
Ошибка сегментирования (core dumped)
$ echo $?
139
$ sudo ./ioperm
writing: 0x00 to 0x378
$ echo $?
139

Код завершения программы (139) выглядит довольно странно, а попытка выполнения фиксируется в системном журнале:

$ dmesg | tail -n1 
[12310.795229] ioperm[4485] general protection ip:80484e5 sp:bf8351b0 error:0 
in ioperm[8048000+1000]

Функции обмена данными вообще никаким образом не возвращают и не устанавливают код ошибочности своего выполнения. Например, если обратиться к несуществующим аппаратным портам:

$ sudo ./ioports 200
/dev/port :
        writing: 0x00 to 0x200
        reading: 0xff from 0x201
ioperm :
        writing: 0x00 to 0x200
        reading: 0xff from 0x201
iopl :
        writing: 0x00 to 0x200
        reading: 0xff from 0x201

Хотя из не существующих портов можно считать 0xff, но это нельзя считать надёжным критерием, поэтому присутствие портов следует контролировать отдельно, например, по содержимому файла /proc/ioports, как это показывалось выше.

Замечание относительно iopl() и уровней привилегий: если этой функции указать параметр больше 3, то она возвратит ошибку:

$ sudo ./ioports 378 4
...
iopl failed: Invalid argument
$ echo $?
22

А поскольку, в Linux из 4-х колец защиты процессора x86 (0, 1, 2, 3) использованы только 2 (0 — для ядра Linux, 3 — для пользовательского пространства), то при вызове iopl() имеет смысл только:

  • iopl( 3 )— разрешить пользовательскому процессу привилегированные операции;
  • iopl( 0 )— возвратить запрет на выполнение пользовательским процессом привилегированных операции.

Ещё один вопрос: нельзя ли (например при отработке прототипов, о чём говорилось в начале) получить доступ из пользовательского пространства к DMA и прерываниям? Нет, для пользовательского API подобные возможности не предоставляются. Но для отработки прототипа можно создать простейший модуль для обработки аппаратного прерывания, который по получению прерывания должен будет отослать сигнал UNIX драйверу пользовательского пространства, как обсуждалось в предыдущих статьях. Так же можно решить вопрос с асинхронными событиями аппаратуры.

Заключение

На этом мы завершим обсуждение различных способов, предлагаемых ОС Linux для взаимодействия с периферийной аппаратурой и пожелаем читателю удачи в продолжении исследований и решении практических задач, связанных с разработкой и поддержкой модулей ядра Linux.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=932898
ArticleTitle=Обслуживание периферии в коде модулей ядра: Часть 62. Работа с драйверами в пользовательском пространстве
publish-date=06062013