Библиотека сайта rus-linux.net
Цилюрик О.И. Модули ядра Linux | ||
Назад | Внешние интерфейсы модуля | Вперед |
Неблокирующий ввод-вывод и мультиплексирование
Здесь я имею в виду реализацию в модуле (драйвере) поддержки операций мультиплексированного ожидания возможности выполнения операций ввода-вывода: select() и poll(). Примеры этого раздела будут много объёмнее и сложнее, чем все предыдущие, в примерах будут использованы механизмы ядра, которые мы ещё не затрагивали, и которые будут рассмотрены далее... Но сложность эта обусловлена тем, что здесь мы начинаем вторгаться в обширную и сложную область: неблокирующие и асинхронные операции ввода-вывода. При первом прочтении этот раздел можно пропустить — на него никак не опирается всё последующее изложение.
Примечание: Наилучшую (наилучшую из известных мне) классификаций типов операций ввода-вывода дал У.Р. Стивенс [19], он выделяет 5 категорий, которые принципиально различаются:
- блокируемый ввод-вывод;
- неблокируемый ввод-вывод;
- мультиплексирование ввода-вывода (функции select() и poll());
- ввод-вывод, управляемый сигналом (сигнал SIGIO);
- асинхронный ввод-вывод (функции POSIX.1 aio_*()).
Примеры использования их обстоятельнейшим образом описаны в книге того же автора [20], на которое мы будем, без излишних объяснений, опираться в своих примерах.
Сложность описания подобных механизмов и написания демонстрирующих их примеров состоит в том, чтобы придумать модель-задачу, которая: а). достаточно адекватно использует рассматриваемый механизм и б). была бы до примитивного простой, чтобы её код был не громоздким, легко анализировался и мог использоваться для дальнейшего развития. В данном разделе мы реализуем драйвер (архив poll.tgz) устройства (и тестовое окружение к нему), которое функционирует следующим образом:
- устройство допускает неблокирующие операции записи (в буфер) — в любом количестве, последовательности и в любое время; операция записи обновляет содержимое буфера устройства и устанавливает указатель чтения в начало нового содержимого;
- устройство чтения может запрашивать любое число байт в последовательных операциях (от 1 до 32767), последовательные чтения приводят к ситуации EOF (буфер вычитан до конца), после чего следующие операции read() или poll() будут блокироваться до обновления данных операцией write();
- может выполняться операция read() в неблокирующем режиме, при исчерпании данных буфера она будет возвращать признак «данные не готовы».
К модулю мы изготовим тесты записи (pecho — подобие echo) и чтения (pcat — подобие cat), но позволяющие варьировать режимы ввода-вывода... И, конечно, с этим модулем должны работать и объяснимо себя вести наши неизменные POSIX-тесты echo и cat. Для согласованного поведения всех составляющих эксперимента, общие их части вынесены в два файла *.h :
poll.h :
#define DEVNAME "poll" #define LEN_MSG 160 #ifndef __KERNEL__ // only user space applications #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 // for kernel space module #include <linux/module.h> #include <linux/miscdevice.h> #include <linux/poll.h> #include <linux/sched.h> #endif
Второй файл (user.h) используют только тесты пространства пользователя, мы их посмотрим позже, а пока — сам модуль устройства:
poll.c :
#include "poll.h" MODULE_LICENSE( "GPL" ); MODULE_AUTHOR( "Oleg Tsiliuric <olej@front.ru>" ); MODULE_VERSION( "5.2" ); static int pause = 100; // задержка на операции poll, мсек. 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 ); 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; } 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; } 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, }; 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 ...
- Если же затребована блокирующая операция чтения, а данные для её выполнения недоступны, вызывающий процесс блокируется;
- Разблокирован читающий процесс будет при выполнении более поздней операции записи (в условиях теста — с другого терминала).
Теперь относительно процессов пространства пользователя. Вот обещанный общий включаемый файл:
user.h :
#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; case 'v': p.vis++; break; default: goto err; } if( par > 0 && ( argc - optind ) < par ) 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[ 40 ] = "/dev/"; int dfd; // дескриптор устройства strcat( name, DEVNAME ); 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; };
Тест записи
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 :
#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 -w
./pcat: invalid option -- 'w' usage: ./pcat [-b][-m][-v] [<read block size>]
- отличие только в необязательном параметре, который на этот раз несёт смысл: размер блока (в байтах), который читать за одну операцию чтения (если он не указан то читается максимальный размер буфера).
И окончательно наблюдаем как это всё работает...
Примечание: У этого набора тестов множество степеней свободы (набором опций), позволяющих наблюдать самые различные операции: блокирующие и нет, с ожиданием на poll() и нет, и др. Ниже показывается только самый характерный набор результатов.
$ sudo insmod poll.ko
$ ls -l /dev/po*
crw-rw---- 1 root root 10, 54 Июн 30 11:57 /dev/poll crw-r----- 1 root kmem 1, 4 Июн 30 09:52 /dev/port
Запись производим сколько угодно раз последовательно:
$ echo qwerqr > /dev/poll
$ echo qwerqr > /dev/poll
$ echo qwerqr > /dev/poll
А вот чтение можем произвести только один раз:
$ cat /dev/poll
qwerqr
При повторной операции чтения:
$ cat /dev/poll
...
12346456
- операция блокируется и ожидает (там, где нарисованы: ...), до тех пор, пока с другого терминала на произведена операция:
$ echo 12346456 > /dev/poll
И, как легко можно видеть, заблокированная операция cat после разблокирования выводит уже новое, обновлённое значение буфера устройства (а не то, которое было в момент запуска cat).
Теперь посмотрим что говорят наши, более детализированные тесты... Вот итог повторного (блокирующегося) чтения, в режиме блокировки на pool() и циклическим чтением по 3 байта:
$ ./pcat -v 3
nonblocked: yes, multiplexed: yes, read block size: 3 bytes interval 43:271 read 3 bytes: xxx interval 00:100 read 3 bytes: xx interval 00:100 read 3 bytes: yyy interval 00:100 read 3 bytes: yyy interval 00:100 read 3 bytes: zz interval 00:100 read 3 bytes: zzz interval 00:100 read 3 bytes: tt interval 00:100 read 1 bytes: interval 00:100 read 0 bytes: read EOF
Выполнение команды блокировалось (на этот раз на pool()) до выполнения (>43 секунд) в другом терминале:
$ ./pecho 'xxxxx yyyyyy zzzzz tt'
interval 00:099 write 21 bytes: xxxxx yyyyyy zzzzz tt
А вот как выглядит неблокирующая операция чтения не ожидающая на pool() (несколько первых строк с интервалом 3 сек. показывают неготовность до обновления данных):
$ ./pcat -v 3 -m
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: 123 interval 00:000 read 3 bytes: 45 interval 00:000 read 3 bytes: 678 interval 00:000 read 3 bytes: 90 interval 00:000 read 0 bytes: read EOF
Опять же, делающая доступными данные операция с другого терминала:
$ ./pecho '12345 67890'
interval 00:099 write 11 bytes: 12345 67890
Предыдущий раздел: | Оглавление | Следующий раздел: |
Счётчик ссылок использования модуля | Блочные устройства |