Содержание


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

Часть 22. Создание драйвера устройства с поддержкой асинхронного ввода-вывода

Comments

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

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

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

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

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

Все операции на устройстве, упоминавшиеся до сих пор (read(), write(), ioctl()) обладали, хотя это и не упоминалось отдельно, одним общим свойством: все они относились к блокирующим операциям (или так называемым синхронным операциям). До этого момента мы рассматривали различные варианты реализации драйвера применительно именно к таким операциям. В этой статье будут рассмотрены некоторые типы операций, выходящие за рамки этого ограничения.

Типы операций

Наиболее полную классификацию операций ввода-вывода по их типам (наилучшую из известных мне) дал У. Р. Стивенс в книге "UNIX Network Programming. Networking APIs" (см. раздел "Ресурсы"). В ней он выделяет 5 принципиально различающихся типов операций:

  1. блокирующий ввод-вывод: когда приложение, запрашивающее операцию, блокируется до момента фактического выполнения операции, например, готовности данных для чтения;
  2. неблокирующий ввод-вывод: когда при невозможности немедленного выполнения операции приложение, запросившее её, не блокируется и продолжает выполняться далее, а сама операция завершается с установленным признаком невозможности её выполнения на данный момент;
  3. мультиплексирование ввода-вывода (функции select() и poll()): когда выполнение операции ожидается на более, чем одном дескрипторе файла;
  4. ввод-вывод, управляемый сигналом (сигнал SIGIO): когда ввод-вывод инициирует начало операции, приложение при этом не блокируется, а о готовности данных к копированию в буфер приложения оно асинхронно уведомляется сигналом UNIX;
  5. асинхронный ввод-вывод (функции из стандарта реального времени POSIX.1g вида aio_*()): такие функции ввода-вывода только инициируют начало выполнения операции, не блокируя приложение; приложение уведомляется асинхронно о завершении выполнения операции, и в отличии от предыдущего случая уведомление содержит сигнал не о возможности начала копирования данных, а о полном завершении копирования данных в буфер приложения.

Актуальность интереса к двум последним типам взаимодействия в некоторой степени снижается в операционных системах, прикладной API которых поддерживает механизм POSIX потоков pthread_t, и Linux как раз принадлежит к классу таких систем. Тем не менее, поддержка операций всех типов делает устройство более гибким и расширяет область его применения.

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

Неблокирующий ввод-вывод и мультиплексирование

В данном разделе будет обсуждаться реализация в коде модуля (драйвера) поддержки:

  • неблокирующих операций read() и write() на устройстве;
  • мультиплексированного ожидания возможности выполнения операций ввода-вывода: select() и poll().

Обе возможности, хотя они и являются независимыми, будут продемонстрированы в едином примере, чтобы избежать дублирования совместно используемого кода. Примеры в этом разделе будут объёмнее и сложнее, чем все показанные ранее, поэтому они потребуют некоторых более детальных разъяснений.

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

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

Далее в статье будет использоваться драйвер устройства и тестовое окружение к нему, исходный код которых можно найти в архиве poll.tgz в разделе "Материалы для скачивания". Наш пример будет функционировать следующим образом:

  • устройство допускает неблокирующие операции записи (в буфер) — в любом количестве, последовательности и в любое время; операция записи обновляет содержимое буфера устройства и устанавливает указатель чтения в начало нового содержимого;
  • операция чтения может запрашивать любое число байт в последовательных операциях (от 1 до 32767), последовательные чтения приводят к ситуации EOF (буфер вычитан до конца), после чего следующие операции read() или poll() будут блокироваться до обновления данных следующей операцией write();
  • операция read() может выполняться и в неблокирующем режиме, тогда при исчерпании данных буфера (достижения конца данных) операция будет немедленно возвращать признак «данные не готовы».

Дополнительно модуль будет снабжен тестами для проверки функционирования записи (pecho — подобие команды echo) и чтения (pcat — подобие команда cat), позволяющими изменять режимы ввода-вывода. И, конечно, этот модуль обязан согласованно работать и со стандартными POSIX-тестами echo и cat. Все составляющие эксперимента (модуль и пользовательские приложения-тесты) для согласованного поведения используют общие определения, вынесенные в файл poll.h.

Листинг 1. Общие определения, вынесенные в файл poll.h.
#define DEVNAME "poll" 
#define LEN_MSG 160  // произвольно выбранный размер внутреннего буфера устройства 

// определения для приложений из пространства пользователя
#ifndef __KERNEL__
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <unistd.h> 
#include <fcntl.h> 
#include <poll.h> 
#include <errno.h> 
#include "user.h" 

// определения для модулей из пространства ядра
#else
#include <linux/module.h> 
#include <linux/miscdevice.h> 
#include <linux/poll.h> 
#include <linux/sched.h> 
#endif

В листинге 2 приведен код модуля-драйвера для нашего устройства (файл poll.c, находящийся в архиве poll.tgz).

Листинг 2. Драйвер устройства.
#include "poll.h" 
MODULE_LICENSE( "GPL" ); 
MODULE_AUTHOR( "Oleg Tsiliuric <olej@front.ru>" ); 
MODULE_VERSION( "5.2" ); 

static int pause = 100;       // задержка на операции poll - 100 мсек.
module_param( pause, int, S_IRUGO ); 

static struct private {       // блок данных устройства 
   atomic_t roff;             // смещение для чтения 
   char buf[ LEN_MSG + 2 ];   // буфер данных 
} devblock = { // статическая инициализация того, что динамически делается в open()
   .roff = ATOMIC_INIT( 0 ), 
   .buf = "not initialized yet!\n", 
}; 
static struct private *dev = &devblock; 

static DECLARE_WAIT_QUEUE_HEAD( qwait ); // очередь ожидания готовности 

// чтение внутреннего буфера (в devblock) до его исчерпания
static ssize_t read( struct file *file, char *buf, 
                     size_t count, loff_t *ppos ) { 
   int len = 0; 
   int off = atomic_read( &dev->roff ); 
   if( off > strlen( dev->buf ) ) {         // нет доступных данных 
      if( file->f_flags & O_NONBLOCK ) 
         return -EAGAIN; 
      else interruptible_sleep_on( &qwait ); 
   } 
   off = atomic_read( &dev->roff );         // повторное обновление 
   if( off == strlen( dev->buf ) ) { 
      atomic_set( &dev->roff, off + 1 ); 
      return 0;                             // EOF 
   } 
   len = strlen( dev->buf ) - off;          // данные есть (появились?) 
   len = count < len ? count : len; 
   if( copy_to_user( buf, dev->buf + off, len ) ) 
      return -EFAULT; 
   atomic_set( &dev->roff, off + len ); 
   return len; 
} 

// полное обновление содержимого буфера (и снятие EOF)
static ssize_t write( struct file *file, const char *buf, 
                      size_t count, loff_t *ppos ) { 
   int res, len = count < LEN_MSG ? count : LEN_MSG; 
   res = copy_from_user( dev->buf, (void*)buf, len ); 
   dev->buf[ len ] = '\0';                  // восстановить завершение строки 
   if( '\n' != dev->buf[ len - 1 ] ) strcat( dev->buf, "\n" ); 
   atomic_set( &dev->roff, 0 );             // разрешить следующее чтение 
   wake_up_interruptible( &qwait ); 
   return len; 
} 

// операция мультиплексирования poll/select (с искусственной задержкой)
unsigned int poll( struct file *file, struct poll_table_struct *poll ) { 
   int flag = POLLOUT | POLLWRNORM; 
   poll_wait( file, &qwait, poll ); 
   sleep_on_timeout( &qwait, pause ); 
   if( atomic_read( &dev->roff ) <= strlen( dev->buf ) ) 
      flag |= ( POLLIN | POLLRDNORM ); 
   return flag; 
}; 

// таблица допустимых операций, выполняемых устройством
static const struct file_operations fops = { 
   .owner = THIS_MODULE, 
   .read  = read, 
   .write = write, 
   .poll  = poll, 
}; 

// структура регистрации устройства 
// (простейшим способом, используя misc_register())
static struct miscdevice pool_dev = { 
   MISC_DYNAMIC_MINOR, DEVNAME, &fops 
}; 

static int __init init( void ) { 
   int ret = misc_register( &pool_dev ); 
   if( ret ) printk( KERN_ERR "unable to register device\n" ); 
   return ret; 
} 
module_init( init ); 

static void __exit exit( void ) { 
   misc_deregister( &pool_dev ); 
} 
module_exit( exit );

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

  • операция poll() вызывает (всегда) poll_wait() для одной (в нашем случае это qwait) или нескольких очередей ожидания (часто это одна очередь для чтения и одна для записи);
  • далее производится анализ доступности условий для выполнения операций записи и чтения, и на основе этого анализа возвращается флаг результата (биты тех операций, которые могут быть выполнены без блокирования);
  • в операции read() может быть указан неблокирующий режим операции: бит O_NONBLOCK в поле f_flags, переданном в параметре struct file;
  • если была затребована блокирующая операция чтения, а данные для её выполнения недоступны, то вызывающий процесс блокируется;
  • читающий процесс будет разблокирован при выполнении более поздней операции записи (в условиях описываемого теста — с другого терминала).

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

Заключение

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


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


Похожие темы


Комментарии

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

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