Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux
Цилюрик О.И. Модули ядра Linux
Назад Внешние интерфейсы модуля Вперед

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

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

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

Детальнее это проще рассмотреть на примере (архив mopen.tgz). У нас будет модуль, реализующий все три названных варианта (параметр mode запуска модуля):

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" );

	static int mode = 0; // открытие: 0 - без контроля, 1 - единичное, 2 - множественное 
	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 :

	#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

Хорошо видно, что при второй попытке открытия устройства возникла ошибка «устройство занято».

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

$ 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, каждая, открывают свой экземпляр устройства, выполняют требуемую операцию и закрывают устройство. Следующая выполняемая команда работает с совершенно другим экземпляром устройства и, соответственно, с другой копией данных! Это косвенно подтверждает и число ссылок на модуль после завершения операций (но этом мы поговорим детально чуть ниже):

$ 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)? Этого категорично утверждать нельзя! Очень часто устройство физического доступа (аппаратная реализация) по своей природе требует только монопольного его использования, и тогда схема множественного параллельного доступа становится неуместной. Опять же, схема множественного доступа (в такой или иной реализации) должна предусматривать динамическое управление памятью, что принято считать более опасным в системах критической надёжности и живучести (но и само это мнение тоже может вызывать сомнения). В любом случае, способ открытия устройства может реализовываться по самым различным алгоритмам, должен соответствовать логике решаемой задачи, накладывает требования на реализацию всех прочих операций на устройстве, и, в итоге, заслуживает самой пристальной проработки при начале нового проекта.


Предыдущий раздел: Оглавление Следующий раздел:
Управляющие операции устройства   Счётчик ссылок использования модуля