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

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

Comments

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

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

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

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

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

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

Тестирование драйвера в естественных условиях

Проверим работу драйвера c помощью естественных тестеров POSIX, как это уже показывалось в предыдущих статьях.

$ cat /dev/poll 
not initialized yet! 
$ echo новое содержимое > /dev/poll 
$ cat /dev/poll 
новое содержимое

Но вот повторное считывание с устройства приведёт к блокированному состоянию:

$ cat /dev/poll 
...

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

$ sh 
sh-4.0$ 
sh-4.0$ echo 1234567 > /dev/poll

И заблокированная ранее команда сама успешно завершится:

$ cat /dev/poll 
1234567

Здесь наблюдается нормальное (запланированное) поведение драйвера. Отметим очень важное обстоятельство, с которым мы встречаемся впервые: команды cat и echo в этом примере выполняются параллельно, и дважды выполняют операцию open() в коде драйвера. Мы отдельно вернёмся к этому вопросу в будущем, при рассмотрении множественного открытия устройства. А сейчас, для изучения и проверки более сложных возможностей драйвера (неблокирующие операции, мультиплексирование) необходимо создать специализированные приложения для тестирования: pecho и pcat. Поскольку они во многом совпадают, то общие части кода (обработка опций и параметров командной строки, открытие устройства по имени, фиксация момента выполнения операций во времени) вынесены в единый включаемый файл пространства пользователя (файл user.h, находящийся в архиве poll.tgz в разделе "Материалы для скачивания").

Листинг 1. Общие фрагменты тестовых программ.
#define ERR(...) fprintf( stderr, "\7" __VA_ARGS__ ), exit( EXIT_FAILURE ) 

struct parm {         // структура признаков, заданных опциями команды запуска 
   int blk, vis, mlt; 
}; 

// разбор опций запуска, записанных в командной строке:
struct parm parms( int argc, char *argv[], int par ) { 
   int c; 
   struct parm p = { 0, 0, 0 }; 
   while( ( c = getopt( argc, argv, "bvm" ) ) != EOF ) 
      switch( c ) { 
         case 'b': p.blk = 1; break;  // блокирующие операции
         case 'm': p.mlt = 1; break;  // не использовать poll() 
         case 'v': p.vis++; break;    // повысить уровень диагностического вывода
         default: goto err; 
   } 
   if( ( par != 0 && ( argc - optind ) != abs( par ) )) goto err; 
   if( par < 0 && atoi( argv[ optind ] ) <= 0 ) goto err; 
   return p; 
err: 
   ERR( "usage: %s [-b][-m][-v] %s\n", argv[ 0 ], par < 0 ? 
        "<read block size>" : "<write string>" ); 
} 

// открытие устройства для операций (если устройство существует)
int opendev( void ) { 
   char name[] = "/dev/"DEVNAME; 
   int dfd;                              // дескриптор устройства 
   if( ( dfd = open( name, O_RDWR ) ) < 0 ) 
      ERR( "open device error: %m\n" ); 
   return dfd; 
} 

void nonblock( int dfd ) {               // операции в режиме O_NONBLOCK 
   int cur_flg = fcntl( dfd, F_GETFL ); 
   if( -1 == fcntl( dfd, F_SETFL, cur_flg | O_NONBLOCK ) ) 
      ERR( "fcntl device error: %m\n" ); 
} 

// форматировать временные интервалы в удобный для восприятия вид 
const char *interval( struct timeval b, struct timeval a ) { 
   static char res[ 40 ]; 
   long msec = ( a.tv_sec - b.tv_sec ) * 1000 + ( a.tv_usec - b.tv_usec ) / 1000; 
   if( ( a.tv_usec - b.tv_usec ) % 1000 >= 500 ) msec++; 
   sprintf( res, "%02d:%03d", msec / 1000, msec % 1000 ); 
   return res; 
};
Листинг 2. Программа для тестирования операции записи (файл pecho.c)
#include "poll.h" 
int main( int argc, char *argv[] ) { 
   struct parm p = parms( argc, argv, 1 ); 
   const char *sout = argv[ optind ]; 
   if( p.vis > 0 ) 
      fprintf( stdout, "nonblocked: %s, multiplexed: %s, string for output: %s\n", 
         ( 0 == p.blk ? "yes" : "no" ), 
         ( 0 == p.mlt ? "yes" : "no" ), 
         argv[ optind ] ); 
   int dfd = opendev();                       // дескриптор устройства 
   if( 0 == p.blk ) nonblock( dfd ); 
   struct pollfd client[ 1 ] = { 
      { .fd = dfd, 
        .events = POLLOUT | POLLWRNORM, 
      } 
   }; 
   struct timeval t1, t2; 
   gettimeofday( &t1, NULL ); 
   int res; 
   if( 0 == p.mlt ) res = poll( client, 1, -1 ); 
   res = write( dfd, sout, strlen( sout ) ); // запись 
   gettimeofday( &t2, NULL ); 
   fprintf( stdout, "interval %s write %d bytes: ", interval( t1, t2 ), res ); 
   if( res < 0 ) ERR( "write error: %m\n" ); 
   else if( 0 == res ) { 
      if( errno == EAGAIN ) 
         fprintf( stdout, "device NOT READY!\n" ); 
   } 
   else fprintf( stdout, "%s\n", sout ); 
   close( dfd ); 
   return EXIT_SUCCESS; 
};

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

$ ./pecho 
usage: ./pecho [-b][-m][-v] <write string>

где:

  • -b — установить блокирующий режим операции (по умолчанию неблокирующий);
  • -m — не использовать ожидание в методе poll() (по умолчанию используется);
  • -v — увеличить степень детализации вывода (для диагностики).

Также параметром задается строка, которая будет записана в устройство /dev/poll, и если строка содержит пробелы или другие спецсимволы, то она должна быть заключена в кавычки. Расширенные (в зависимости от используемых опций) возможности программы тестирования записи, в данном случае не используются, так как они не требуются для нашего драйвера. Это было сделано, чтобы не усложнять изложение. В более реалистичном сценарии драйвер при записи должен был бы блокироваться при не пустом (не вычитанном до конца) буфере устройства. И вот тогда все возможности теста окажутся востребованными.

Программу для тестирования чтения (файл pcat.c) можно найти в архиве poll.tgz в разделе "Материалы для скачивания".

Листинг 3. Программа для тестирования чтения
#include "poll.h" 
int main( int argc, char *argv[] ) { 
   struct parm p = parms( argc, argv, -1 ); 
   int blk = LEN_MSG; 
   if( optind < argc && atoi( argv[ optind ] ) > 0 ) 
      blk = atoi( argv[ optind ] ); 
   if( p.vis > 0 ) 
      fprintf( stdout,          // диагностика опций, с которыми запускались
               "nonblocked: %s, multiplexed: %s, read block size: %s bytes\n", 
         ( 0 == p.blk ? "yes" : "no" ), 
         ( 0 == p.mlt ? "yes" : "no" ), 
         argv[ optind ] ); 
   int dfd = opendev();                 // дескриптор устройства 
   if( 0 == p.blk ) nonblock( dfd ); 
   struct pollfd client[ 1 ] = { 
      { .fd = dfd, 
        .events = POLLIN | POLLRDNORM, 
      } 
   }; 
   while( 1 ) { 
      char buf[ LEN_MSG + 2 ];          // буфер данных 
      struct timeval t1, t2; 
      int res; 
      gettimeofday( &t1, NULL ); 
      if( 0 == p.mlt ) res = poll( client, 1, -1 ); 
      res = read( dfd, buf, blk );      // чтение 
      gettimeofday( &t2, NULL ); 
      fprintf( stdout, "interval %s read %d bytes: ", 
               interval( t1, t2 ), res ); 
      fflush( stdout ); 
      if( res < 0 ) { 
         if( errno == EAGAIN ) { 
            fprintf( stdout, "device NOT READY\n" ); 
            if( p.mlt != 0 ) sleep( 3 ); 
         } 
         else 
            ERR( "read error: %m\n" ); 
      } 
      else if( 0 == res ) { 
         fprintf( stdout, "read EOF\n" ); 
         break; 
      } 
      else { 
         buf[ res ] = '\0'; 
         fprintf( stdout, "%s\n", buf ); 
      } 
   } 
   close( dfd ); 
   return EXIT_SUCCESS; 
};

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

$ ./pcat -v
usage: ./pcat [-b][-m][-v] <read block size>

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

$ echo '12.34.56.78.' > /dev/poll 
$ ./pcat -v 3 
nonblocked: yes, multiplexed: yes, read block size: 3 bytes 
interval 00:099 read 3 bytes: 12. 
interval 00:100 read 3 bytes: 34. 
interval 00:100 read 3 bytes: 56. 
interval 00:100 read 3 bytes: 78. 
interval 00:100 read 1 bytes: 

interval 00:100 read 0 bytes: read EOF

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

$ ./pcat -v 3 
nonblocked: yes, multiplexed: yes, read block size: 3 bytes 
interval 03:143 read 3 bytes: 12. 
interval 00:100 read 3 bytes: 34. 
interval 00:100 read 3 bytes: 56. 
interval 00:100 read 3 bytes: 78. 
interval 00:100 read 1 bytes: 

interval 00:100 read 0 bytes: read EOF 
sh-4.0$ ./pecho -v '12.34.56.78.' 
nonblocked: yes, multiplexed: yes, string for output: 12.34.56.78. 
interval 00:100 write 12 bytes: 12.34.56.78.

Это результат выполнения мультиплексной функции poll(). В реализацию операции poll() в коде драйвера была искусственно введена задержка срабатывания 100 мсек через параметр конфигурации модуля — pause. Благодаря этому, гарантировано видно, что срабатывает именно функция poll(). Для сравнения, вот как выглядит результат тестирования, когда блокирование происходит в блокирующем методе read(), а не poll() (также, как это делает команда cat):

$ ./pcat -v -m -b 3 
nonblocked: no, multiplexed: no, read block size: 3 bytes 
interval 04:812 read 3 bytes: 12. 
interval 00:000 read 3 bytes: 34. 
interval 00:000 read 3 bytes: 56. 
interval 00:000 read 3 bytes: 78. 
interval 00:000 read 1 bytes: 

interval 00:000 read 0 bytes: read EOF

Аналогичным образом можно протестировать другую возможность, поддерживаемую драйвером — неблокирующие операции, не ожидающие возможности её завершения:

$ ./pcat -v -m 3 
nonblocked: yes, multiplexed: no, read block size: 3 bytes 
interval 00:000 read -1 bytes: device NOT READY 
interval 00:000 read -1 bytes: device NOT READY 
interval 00:000 read -1 bytes: device NOT READY 
interval 00:000 read -1 bytes: device NOT READY 
interval 00:000 read 3 bytes: 12. 
interval 00:000 read 3 bytes: 34. 
interval 00:000 read 3 bytes: 56. 
interval 00:000 read 3 bytes: 78. 
interval 00:000 read 1 bytes: 

interval 00:000 read 0 bytes: read EOF

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

Заключение

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


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


Похожие темы


Комментарии

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

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