Разработка модулей ядра Linux

Часть 24. Драйвер устройства. Множественное открытие устройства

Comments

Серия контента:

Этот контент является частью # из серии # статей: Разработка модулей ядра Linux

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Разработка модулей ядра Linux

Следите за выходом новых статей этой серии.

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

Множественное открытие устройства

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

  1. драйвер вообще не контролирует возможность параллельного использования (то, что было во многих рассматриваемых примерах);
  2. драйвер допускает только единственное открытие устройства; попытки параллельного открытия будут завершаться ошибкой ("устройство занято") до тех пор, пока процесс, уже использующий устройство, не закроет его;
  3. в предыдущем случае, как вариант, операция повторного открытия устройства может блокироваться до момента освобождения устройства;
  4. драйвер допускает множество параллельных сессий использования устройства; при этом он должен реализовать индивидуальный экземпляр данных для каждой копии открытого устройства.

Более детально это лучше рассмотреть на примере (архив mopen.tgz с исходным кодом примера находится в разделе "Материалы для скачивания"). Мы воспользуемся модулем, реализующим все три названных варианта (выбор поведения осуществляется через параметр mode при запуске модуля).

Листинг 1. Исходный код драйвера (файл mmopen.c).
#include <linux/module.h> 
#include <linux/fs.h> 
#include <asm/uaccess.h> 
#include <linux/miscdevice.h> 
#include "mopen.h" 

MODULE_LICENSE( "GPL" ); 
MODULE_AUTHOR( "Oleg Tsiliuric <olej@front.ru>" ); 
MODULE_VERSION( "5.4" ); 

// открытие: 0 - без контроля, 1 - единичное, 2 - множественное 
static int mode = 0; 
module_param( mode, int, S_IRUGO ); 
static int debug = 0; 
module_param( debug, int, S_IRUGO ); 

#define LOG(...) if( debug !=0 ) printk( KERN_INFO __VA_ARGS__ ) 

static int dev_open = 0; 

static int mopen_open( struct inode *n, struct file *f ) { 
   LOG( "open - node: %p, file: %p, refcount: %d", 
        n, f, module_refcount( THIS_MODULE ) ); 
   if( dev_open ) return -EBUSY; 
   if( 1 == mode ) dev_open++;   // только один экземпляр!
   if( 2 == mode ) {             // для каждого открытия — свои данные
      f->private_data = kmalloc( LEN_MSG + 1, GFP_KERNEL ); 
      if( NULL == f->private_data ) return -ENOMEM; 
      // динамический буфер 
      strcpy( f->private_data, "dynamic: not initialized!" ); 
   } 
   return 0; 
} 

static int mopen_release( struct inode *n, struct file *f ) { 
   LOG( "close - node: %p, file: %p, refcount: %d",
        n, f, module_refcount( THIS_MODULE ) ); 
   if( 1 == mode ) dev_open--; 
   if( 2 == mode ) kfree( f->private_data ); 
   return 0; 
} 

static char* get_buffer( struct file *f ) { 
   // статический буфер
   static char static_buf[ LEN_MSG + 1 ] = "static: not initialized!"; 
   switch( mode ) { 
      case 0: 
      case 1: 
      default: 
         return static_buf; 
      case 2:     // здесь для каждого экземпляра свой буфер      
         return (char*)f->private_data; 
   } 
} 

// чтение из /dev/mopen : 
static ssize_t mopen_read( struct file *f, char *buf, 
                           size_t count, loff_t *pos ) { 
   static int odd = 0; 
   char *buf_msg = get_buffer( f );  // выбираем конкретный буфер
   LOG( "read - file: %p, read from %p bytes %d; refcount: %d", 
        f, buf_msg, count, module_refcount( THIS_MODULE ) ); 
   if( 0 == odd ) { 
      int res = copy_to_user( (void*)buf, buf_msg, strlen( buf_msg ) ); 
      odd = 1; 
      put_user( '\n', buf + strlen( buf_msg ) ); 
      res = strlen( buf_msg ) + 1; 
      LOG( "return bytes :  %d", res ); 
      return res; 
   } 
   odd = 0; 
   LOG( "return : EOF" ); 
   return 0; 
} 

// запись в /dev/mopen : 
static ssize_t mopen_write( struct file *f, const char *buf, 
                            size_t count, loff_t *pos ) { 
   int res, len = count < LEN_MSG ? count : LEN_MSG; 
   char *buf_msg = get_buffer( f );  // выбираем конкретный буфер
   LOG( "write - file: %p, write to %p bytes %d; refcount: %d", 
        f, buf_msg, count, module_refcount( THIS_MODULE ) ); 
   res = copy_from_user( buf_msg, (void*)buf, len ); 
   if( '\n' == buf_msg[ len -1 ] ) buf_msg[ len -1 ] = '\0'; 
   else buf_msg[ len ] = '\0'; 
   LOG( "put bytes : %d", len ); 
   return len; 
} 

static const struct file_operations mopen_fops = { 
   .owner  = THIS_MODULE, 
   .open =    mopen_open, 
   .release = mopen_release, 
   .read   =  mopen_read, 
   .write  =  mopen_write, 
}; 

static struct miscdevice mopen_dev = { 
   MISC_DYNAMIC_MINOR, DEVNAM, &mopen_fops 
}; 

static int __init mopen_init( void ) { 
   int ret = misc_register( &mopen_dev ); 
   if( ret ) printk( KERN_ERR "unable to register %s misc device", DEVNAM ); 
   return ret; 
}

static void __exit mopen_exit( void ) { 
   misc_deregister( &mopen_dev ); 
} 

module_init( mopen_init ); 
module_exit( mopen_exit );

Начнём тестирование модуля со стандартных команд чтения и записи: cat и echo, но этого будет недостаточно. Поэтому мы также воспользуемся специально разработанным тестовым приложением (файл pmopen.c), выполняющим одновременное открытие двух дескрипторов нашего устройства и делающим на каждом из них поочерёдные операции записи-чтения (но порядок выполнения операций чтения обратен порядку операций записи). Записываются, соответственно, две произвольно выбранные строки ("1111111" и "22222") по которым можно различать содержимое внутреннего буфера модуля.

Листинг 2. Тестовое приложение
#include <fcntl.h>
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include "mopen.h" 

char dev[ 80 ] = "/dev/"; 

int prepare( char *test ) { 
   int df; 
   if( ( df = open( dev, O_RDWR ) ) < 0 ) 
      printf( "open device error: %m\n" ); 
   int res, len = strlen( test ); 
   if( ( res = write( df, test, len ) ) != len ) 
      printf( "write device error: %m\n" ); 
   else 
      printf( "prepared %d bytes: %s\n", res, test ); 
   return df; 
} 

void test( int df ) { 
   char buf[ LEN_MSG + 1 ]; 
   int res; 
   printf( "------------------------------------\n" ); 
   do { 
      if( ( res = read( df, buf, LEN_MSG ) ) > 0 ) { 
         buf[ res ] = '\0'; 
         printf( "read %d bytes: %s\n", res, buf ); 
      } 
      else if( res < 0 ) 
         printf( "read device error: %m\n" ); 
      else 
         printf( "read end of stream\n" ); 
   } while ( res > 0 ); 
   printf( "------------------------------------\n" ); 
} 

int main( int argc, char *argv[] ) { 
   strcat( dev, DEVNAM ); 
   int df1, df2;                   // разные дескрипторы одного устройства 
   df1 = prepare( "1111111" ); 
   df2 = prepare( "22222" ); 
   test( df1 ); 
   test( df2 ); 
   close( df1 ); 
   close( df2); 
   return EXIT_SUCCESS; 
};

Для совместимости модуль и приложение используют общий заголовочный файл mopen.h, приведенный ниже:

#define DEVNAM "mopen"   // имя устройства 
#define LEN_MSG 256      // длины буферов устройства

Хотя пример получился несколько объемным, но он даёт ощущение тонкого разграничения деталей возможных реализаций в концепции устройства! Итак, первый вариант, когда драйвер вообще никоим образом не контролирует открытия устройства (параметр mode можно не указывать — это значение используется по умолчанию, здесь он явно указан для единообразия):

$ sudo insmod ./mmopen.ko mode=0 
$ cat /dev/mopen 
static: not initialized!

Запишем на устройство произвольную символьную строку:

$ echo 777777777 > /dev/mopen 
$ cat /dev/mopen 
777777777 
$ ./pmopen 
prepared 7 bytes: 1111111 
prepared 5 bytes: 22222 
----
read 6 bytes: 22222 

read end of stream 
----
----
read 6 bytes: 22222 

read end of stream 
----
$ sudo rmmod mmopen

Здесь мы наблюдаем нормальную работу драйвера устройства при тестировании его утилитами POSIX (echo/cat) — это важный этап проверки, с которого всегда следует начинать. Но в контексте множественного доступа происходит определённо не то, что ожидалось: две операции записи пишут в единый статический буфер устройства, а два последующих чтения, естественно, считывают идентичные значения, записанные последней операцией записи.

При следующем запуске устройство допускает только единичные (монопольные) операции доступа, и до тех пор, пока использующий процесс его не освободит, все последующие попытки использовать устройство будут безуспешны:

$ sudo insmod ./mmopen.ko mode=1
$ cat /dev/mopen 
static: not initialized! 
$ echo 777777777 > /dev/mopen 
$ cat /dev/mopen 
777777777 
$ ./pmopen 
prepared 7 bytes: 1111111 
open device error: Device or resource busy 
write device error: Bad file descriptor 
----
read 8 bytes: 1111111 

read end of stream 
----
----
read device error: Bad file descriptor 
----
$ sudo rmmod mmopen

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

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

$ sudo insmod ./mmopen.ko mode=2
$ cat /dev/mopen 
dynamic: not initialized! 
$ echo 777777777 > /dev/mopen 
$ cat /dev/mopen 
dynamic: not initialized!

Это очень странный результат и понять, что происходит, нам поможет отладочный режим загрузки модуля (для этого добавлен параметр запуска debug, без которого модуль ничего не пишет в системный журнал, чтобы не засорять его) и содержимое системного журнала (показанный вывод в точности соответствует последовательности команд, приведенной выше).

$ sudo insmod ./mmopen.ko mode=2 debug=1 
$ echo 9876543210 > /dev/mopen 
$ cat /dev/mopen 
dynamic: not initialized! 
$ dmesg | tail -n10
open - node: f2e855c0, file: f2feaa80, refcount: 1 
write - file: f2feaa80, write to f2c5f000 bytes 11; refcount: 1 
put bytes : 11 
close - node: f2e855c0, file: f2feaa80, refcount: 1 
open - node: f2e855c0, file: f2de2d80, refcount: 1 
read - file: f2de2d80, read from f2ff9600 bytes 32768; refcount: 1 
return bytes :  26 
read - file: f2de2d80, read from f2ff9600 bytes 32768; refcount: 1 
return : EOF 
close - node: f2e855c0, file: f2de2d80, refcount: 1

Каждая из тестовых операций echo и cat открывает свой экземпляр устройства, выполняет требуемую операцию и закрывает устройство. Следующая выполняемая команда (cat после echo) работает с совершенно другим экземпляром устройства и, соответственно, с другой копией данных! Это косвенно подтверждает и число ссылок на модуль после завершения операций (но об этом мы поговорим в следующей статье).

$ lsmod | grep mmopen 
mmopen                  2459  0

Но именно то, для чего был предназначен драйвер - множественное открытие дескрипторов устройства работает отлично.

$ ./pmopen 
prepared 7 bytes: 1111111 
prepared 5 bytes: 22222 
----
read 8 bytes: 1111111 

read end of stream 
----
----
read 6 bytes: 22222 

read end of stream 
----
$ sudo rmmod mmopen 
$ dmesg | tail -n60 
open - node: f2e85950, file: f2f35300, refcount: 1 
write - file: f2f35300, write to f2ff9600 bytes 7; refcount: 1 
put bytes : 7 
open - node: f2e85950, file: f2f35900, refcount: 2 
write - file: f2f35900, write to f2ff9200 bytes 5; refcount: 2 
put bytes : 5 
read - file: f2f35300, read from f2ff9600 bytes 256; refcount: 2 
return bytes :  8 
read - file: f2f35300, read from f2ff9600 bytes 256; refcount: 2 
return : EOF 
read - file: f2f35900, read from f2ff9200 bytes 256; refcount: 2 
return bytes :  6 
read - file: f2f35900, read from f2ff9200 bytes 256; refcount: 2 
return : EOF 
close - node: f2e85950, file: f2f35300, refcount: 2 
close - node: f2e85950, file: f2f35900, refcount: 1

Заключение

Рассмотрим вопрос: всегда ли последний вариант (mode=2) лучше других (mode=0 или mode=1)? Категорично это утверждать нельзя, так как часто устройство физического доступа (аппаратная реализация) по своей природе требует только монопольного использования, и тогда схема множественного параллельного доступа становится неуместной. Также схема множественного доступа (в той или иной реализации) должна предусматривать динамическое управление памятью, что принято считать более опасным в системах критической надёжности (правда, само это мнение тоже может вызывать сомнения). В любом случае, способ открытия устройства может реализоваться по самым различным алгоритмам, но должен соответствовать логике решаемой задачи и определяет требования к реализации остальных операций. Поэтому способ открытия устройства заслуживает самой пристальной проработки при начале нового проекта.


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


Похожие темы


Комментарии

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

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