Содержание


Инструменты ОС Linux для разработчиков приложений для ОС Windows. Часть 17. Расширенные операции ввода-вывода

Comments

В этой статье, последней в данном цикле публикаций, мы завершим рассмотрение специфических возможностей POSIX API. Нам осталось изучить особенности реализации API ввода/вывода в системах семейства POSIX (включая ОС Linux), а более конкретно - синхронный-асинхронный ввод-вывод и вызовы select(), pselect(), poll(), epoll().

Модели ввода-вывода

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

  1. блокируемый ввод-вывод;
  2. неблокируемый ввод-вывод;
  3. мультиплексирование ввода-вывода (функции select() и poll());
  4. ввод-вывод, управляемый сигналом (сигнал SIGIO);
  5. асинхронный ввод-вывод (функции POSIX.1 aio_).

Блокируемый ввод используется чаще всего, и самые известные примеры — это вызовы read() или элементарные вызовы getchar() или gets(), выполняемые в каноническом режиме ввода с терминала (консоли). Эта модель ввода-вывода не нуждается в детальных комментариях.

Неблокирующий ввод-вывод

Неблокирующий ввод-вывод не ожидает наличия данных (или возможности вывода), а результат выполнения операции или невозможность её выполнения в данный момент определяется по анализу кода возврата. Все примеры из данной статьи можно найти в архиве ufd.tgz в разделе "Материалы для скачивания". В листинге 1 представлен фрагмент кода с примером неблокирующего ввода.

Листинг 1. Неблокирующий ввод-вывод (файл e5.cc)
int fo[ 2 ];         // pipe–канал для чтения из дочернего процесса
if( pipe( fo ) ) perror( "pipe" ), exit( EXIT_FAILURE );
close( fo[ 1 ] );
int cur_flg = fcntl( fo[ 0 ], F_GETFL );  // чтение должно быть в режиме O_NONBLOCK
if( -1 == fcntl( fo[ 0 ], F_SETFL, cur_flg | O_NONBLOCK ) )
   perror( "fcntl" ), exit( EXIT_FAILURE );
...
while( 1 ) {
   int n = read( fdi, buf, buflen );
   if( n > 0 ) {
                                         // данные считаны ...
                                         // обработка
   }
   else if( -1 == n ) {
      if( EAGAIN == errno ) {            // данные не готовы
         printf( "not ready!\n" );
         usleep( 300 );
         continue;
      }
      else perror( "\nread pipe" ), exit( EXIT_FAILURE );
   }
}

Дополнительную подборку примеров, относящихся к неблокирующему вводу-выводу и использующих сетевые сокеты, можно найти в каталоге nonblock архива ufd.tgz в разделе "Материалы для скачивания" (Примеры для этой части были взяты из книги У. Р. Стивенса, или написаны как вариации по её мотивам).

Мультиплексирование ввода-вывода

Для подобной функциональности в POSIX API предлагаются две реализации: старая на основе вызова select() и новая на основе pselect():

int select( int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
           struct timeval *timeout );
int  pselect( int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
              const struct timespec *timeout, sigset_t *sigmask );

Вызов select() использует тайм-аут в виде struct timeval (с секундами и микросекундами), а в pselect() для этой же цели используется struct timespec (с секундами и наносекундами).

Вызов select() может изменить значение параметра timeout, чтобы сообщить, сколько времени осталось, а вызов pselect() не поддерживает такую функциональность.

Вызов select() не содержит параметра sigmask, и ведет себя как pselect() с параметром sigmask равным NULL. Если этот параметр для pselect() не равен NULL, то pselect() сначала замещает текущую маску сигналов на переданную в sigmask, а затем выполняет select() и восстанавливает исходную маску сигналов.

Параметр тайм-аута может задаваться несколькими способами:

  1. NULL, что означает ожидать вечно;
  2. ожидать конкретный указанный период времени;
  3. не ожидать вообще (программный опрос, pooling), когда структура тайм-аута инициализируется значением {0, 0}.

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

Кроме того вводится понятие набора дескрипторов и макросы для работы с ними

FD_CLR( int fd, fd_set *set );
FD_ISSET( int fd, fd_set *set );
FD_SET( int fd, fd_set *set );
FD_ZERO( fd_set *set );

Ещё один вариант мультиплексирования ввода-вывода вывода предлагается в функции poll(), где представление битового набора дескрипторов заменено на массив структур вида:

struct pollfd {
   int fd;           /* файловый дескриптор */
   short events;     /* запрошенные события */
   short revents;    /* возвращенные события */
};
  • fd — открытый файловый дескриптор;
  • events — набор битовых флагов запрошенных событий для этого дескриптора;
  • revents — набор битовых флагов, возвращенных событием для этого дескриптора (из числа запрошенных, или POLLERR, POLLHUP, POLLNVAL).

Возможные комбинации битов описаны в файлах <sys/poll.h> и <asm/poll.h>:

#define POLLIN      0x0001    /* Можно читать данные */
#define POLLPRI     0x0002    /* Есть срочные данные */
#define POLLOUT     0x0004    /* Запись не будет блокирована */
#define POLLERR     0x0008    /* Произошла ошибка */
#define POLLHUP     0x0010    /* Разрыв соединения */
#define POLLNVAL    0x0020    /* Неверный запрос: fd не открыт */

Сам вызов poll(), объявленный в файле <sys/poll.h>, оперирует с массивом таких структур, где каждому дескриптору соответствует один элемент:

int poll( struct pollfd *ufds, unsigned int nfds, int timeout );
  • ufds - сам массив структур;
  • nfds - его размерность;
  • timeout – величина тайм-аута в миллисекундах (ожидание при положительном значении, немедленный возврат при нулевом, бесконечное ожидание при значении, заданном константой INFTIM).

В листинге 2 представлены фрагменты приложений, использующий вызовы select() и poll(), полный код которых можно найти в архиве ufd.tgz. Примеры представляют собой полноценные TCP-приложения, основанные на архитектуре клиент-сервер, поэтому в листингах показаны только фрагменты, непосредственно относящиеся к вызовам select() и poll().

Листинг 2. Пример ретранслирующего TCP-сервера на основе вызова select() (файл tcpservselect01.c)
...
   int                             nready, client[ FD_SETSIZE ];
   fd_set                          rset, allset;
   socklen_t                       clilen;
   struct sockaddr_in      cliaddr, servaddr;
   ...
   listenfd = Socket( AF_INET, SOCK_STREAM, 0 );
   bzero( &servaddr, sizeof(servaddr) );
   servaddr.sin_family      = AF_INET;
   servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
   servaddr.sin_port        = htons(SERV_PORT);
   Bind( listenfd, (SA*)&servaddr, sizeof(servaddr) );
   Listen( listenfd, LISTENQ );
   maxfd = listenfd;                           /* инициализация */
   maxi = -1;
   for( i = 0; i < FD_SETSIZE; i++ )
      client[i] = -1;
      FD_ZERO( &allset );
      FD_SET( listenfd, &allset );
      for ( ; ; ) {
         rset = allset;                        /* присвоение структуры */
         nready = Select( maxfd + 1, &rset, NULL, NULL, NULL );
         if( FD_ISSET( listenfd, &rset ) ) {   /* подключение нового клиента */
         connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);
...
Листинг 3. Пример ретранслирующего TCP-сервера на основе вызова poll() (файл tcpservpoll01.c)
   struct pollfd           client[ OPEN_MAX ];
   struct sockaddr_in      cliaddr, servaddr;
...
   listenfd = Socket( AF_INET, SOCK_STREAM, 0 );
   bzero( &servaddr, sizeof(servaddr) );
   servaddr.sin_family      = AF_INET;
   servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
   servaddr.sin_port        = htons( SERV_PORT );
   Bind( listenfd, (SA*)&servaddr, sizeof(servaddr) );
   Listen( listenfd, LISTENQ );
   client[0].fd = listenfd;
   client[0].events = POLLRDNORM;
   for( i = 1; i < OPEN_MAX; i++ )
      client[i].fd = -1;
      maxi = 0;
      for ( ; ; ) {
         nready = Poll( client, maxi + 1, INFTIM );
         if( client[0].revents & POLLRDNORM ) {   /* подключение нового клиента */
            for( i = 1; i < OPEN_MAX; i++ )
               if( client[i].fd < 0 ) {
                  client[i].fd = connfd;  /* сохранение дескриптора */
                  break;
               }
...
               client[i].events = POLLRDNORM;
               if( i > maxi )
                  maxi = i;
...

Запуск сервера выполняется через консоль, а для остановки используется сочетание клавиш Ctrl+C. Оба представленных сервера прослушивают фиксированный порт 9877 и ретранслируют клиенту данные, которые только что получили от него:

$ ./tcpservselect01
...
^C

или

$ ./tcpservpoll01
...
^C

Проверить то, что сервер прослушивает порт и готов к работе, можно следующим образом:

$ netstat -a | grep :9877
tcp        0      0 *:9877                      *:*                         LISTEN

Для подключения к серверу используется клиентское приложение из архива ufd.tgz. После запуска приложения в него можно вводить строки, которые будут передаваться на сервер и ретранслироваться обратно:

$ ./tcpcli01 127.0.0.1
1 строка
1 строка
2 строка
2 строка
последняя
последняя
^C

При запуске клиента обязательно нужно указать IP адрес (а не имя) сервера. Оба сервера поддерживают работу с несколькими клиентами одновременно, и во время работы клиента можно увидеть состояние сокетов — клиентского и серверных, прослушивающего и присоединённого (клиент не закрывает соединение после обслуживания каждого запроса):

$ netstat -a | grep :9877
tcp   0      0 *:9877                  *:*                         LISTEN
tcp   0      0 localhost:46783         localhost:9877              ESTABLISHED
tcp   0      0 localhost:9877          localhost:46783             ESTABLISHED

Ввод-вывод, управляемый сигналом

При использовании данного способа на сетевом сокете включается режим ввода-вывода, управляемого сигналом, а обработчик сигнала устанавливается при помощи вызова sigaction().

В листинге 3 представлен пример подобного ввода-вывода, но уже на основе протокола UDP. Полный код примеров можно найти в каталоге sigio в архиве ufd.tgz, но так как он слишком объёмен для данной статьи, то мы ограничимся запуском примера и анализом результатов.

Так при получении UDP-дейтаграммы будет генерироваться сигнал SIGIO, а обработка данные будет происходить в обработчике сигнала, который в свою очередь будет активироваться при вызове recvfrom().

Пример с UDP-сервером запускается и работает точно так, как и ранее рассмотренный пример с TCP-сервером:

$ ./udpserv01
^C
…
$ ./udpcli01 127.0.0.1
qweqert
qweqert
134534256
134534256
^C

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

$ ps -A | grep udp
 2692 pts/12   00:00:00 udpserv01
$ kill -HUP 2692
$ ./udpserv01
cntread[0] = 0
cntread[1] = 8
cntread[2] = 0
cntread[3] = 0
cntread[4] = 0
cntread[5] = 0
cntread[6] = 0
cntread[7] = 0
cntread[8] = 0
^C

Асинхронный ввод-вывод

Асинхронный ввод-вывод был добавлен только в редакции стандарта POSIX.1g в 1993 году, как одно из расширений реального времени. В вызове aio_read() даётся указание ядру начать операцию ввода-вывода, и указывается, каким сигналом уведомить процесс о завершении операции (включая копирование данных в пользовательский буфер). При этом вызывающий процесс не блокируется, а результат операции (например, полученная UDP-дейтаграмма) может быть обработан в обработчике сигнала. Разница с предыдущей моделью ввода-вывода, управляемого сигналом, состоит в том, что в ранее рассмотренной модели сигнал уведомлял о возможности начала операции (вызове операции чтения), а в асинхронной модели сигнал уведомляет уже о завершении операции чтения в буфер пользователя.

Вся функциональность, относящаяся к асинхронному вводу-выводу в Linux, описана в файле <aio.h>. Информация, необходимая для работы с асинхронным вводом-выводом, хранится в структуре aiocb (для 64-х битных операций существует аналогичная структура aiocb64):

struct aiocb {     /* структура управления асинхронным вводом-выводом.  */
   int aio_fildes;               /* дескриптор файла  */
   int aio_lio_opcode;           /* выполняемая операция.  */
   int aio_reqprio;              /* смещение  */
   volatile void *aio_buf;       /* адрес буфера.  */
   size_t aio_nbytes;            /* длина передаваемой последовательности  */
   struct sigevent aio_sigevent; /* номер и значение сигнала.  */
...
}

Представим несколько операций используемых при асинхронном вводе-выводе:

int aio_read( struct aiocb *__aiocbp );
int aio_write( struct aiocb *__aiocbp );
int lio_listio( int __mode, struct aiocb* const list[ __restrict_arr ],
                int __nent, struct sigevent *__restrict __sig ) ;
int aio_cancel( int __fildes, struct aiocb *__aiocbp );

Предпоследний вызов позволяет инициализировать выполнение целой цепочки асинхронных операций (длиной __nent).

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

Также можно предположить, что каждая асинхронная операция выполняется как отдельный поток.

Терминал, режим ввода: канонический и неканонический

Частным случаем блокирующего-неблокирующего вывода является ввод с терминала, поэтому возникает задача, как реализовать неблокирующий посимвольный ввод с терминала/консоли. На самом деле, для неблокирующего ввода не существует какого-то специального набора вызовов POSIX API, а просто используется соответствующий набор параметров терминала, и, в частности, канонический или неканонический режим ввода. Текущие установленные параметры терминала можно посмотреть следующей командой.

$ stty -a < /dev/tty
speed 38400 baud; rows 33; columns 93; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?;
swtch = M-^?; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W;
lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon
-ixoff -iuclc ixany imaxbel iutf8 opost -olcuc -ocrnl onlcr -onocr
-onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe echok
-echonl -noflsh -xcase -tostop -echoprt echoctl echoke

В данном контексте наибольший интерес представляют параметры: icanon, echoV, min, time.

Терминальная система и команда stty были реализованы очень давно, когда в большинстве случаев подключение терминала производилось через последовательные линии RS-232, поэтому очень много параметров терминала ориентированы именно на такое подключение. Но, если сейчас возникает необходимость использовать RS-232 (например, для связи с устройствами), то существует ещё одна команда, выводящая дополнительную диагностическую информацию:

$ sudo setserial -bg /dev/ttyS*
/dev/ttyS0 at 0x03f8 (irq = 4) is a 16550A
/dev/ttyS1 at 0x02f8 (irq = 3) is a 16550A

Для обмена информацией и управления терминалом используется программная структура termios, объявленная в файле <bits/termios.h>.

#define NCCS 32
struct termios {
   tcflag_t c_iflag;           /* флаги режима ввода */
   tcflag_t c_oflag;           /* флаги режима вывода */
   tcflag_t c_cflag;           /* флаги режима управления */
   tcflag_t c_lflag;           /* флаги локального режима */
   cc_t c_line;                /* дисциплина линии, не используется в POSIX */
   cc_t c_cc[NCCS];            /* управляющие символы */
   speed_t c_ispeed;           /* скорость ввода */
   speed_t c_ospeed;           /* скорость вывода */
};

Для работы с этой структурой используются следующие функции:

int tcgetattr( int fd, struct termios *termios );
int tcsetattr( int fd, int optional_actions, const struct termios *termios );

Параметр optional_actions указывает, как поступать с вводом и выводом, уже поставленным в очередь. Его возможные значения также объявлены в файле <bits/termios.h>:

  • TCSANOW - применить требуемые изменения немедленно;
  • TCSADRAIN - применить изменения после ожидания, пока весь поставленный в очередь вывод не выведен (обычно используется при изменении параметров, которые воздействуют на вывод);
  • TCSAFLUSH - подобен TCSADRAIN, но отбрасывает любой поставленный в очередь ввод.

Представленной информации вполне достаточно, чтобы изучить пример из листинга 4, где для прямого управления курсором на экране терминала применяются клавиши d, u, l, r — вверх, вниз, влево, вправо, и q — выход из программы. Полный код примера можно найти в архиве terminal.tgz в разделе "Материалы для скачивания".

Листинг 4. Управление терминалом (файл move.c)
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <termios.h>
#include <stdio.h>

int main ( int argc, char **argv ) {
   struct termios savetty, tty;
   char ch;
   int x, y;
   printf( "Enter start position (x y): " );
   scanf( "%d %d", &x, &y );
   if( !isatty( 0 ) ) {
      fprintf( stderr, "stdin not terminal\n" );
      exit( EXIT_FAILURE );
   };
   tcgetattr( 0, &tty );             // получили состояние терминала
   savetty = tty;
   tty.c_lflag &= ~( ICANON | ECHO | ISIG );
   tty.c_cc[ VMIN ] = 1;
   tcsetattr( 0, TCSAFLUSH, &tty ); // изменили состояние терминала
   printf( "%c[2J", 27 );           // очистили экран
   fflush( stdout );
   printf( "%c[%d;%dH", 27, y, x ); // установили курсор в позицию
   fflush( stdout );
   for( ; ; ) {
      read( 0, &ch, 1 );
      if( ch == 'q' ) break;
      switch( ch ) {
      case 'u':
         printf( "%c[1A", 27 );
         break;
      case 'd':
         printf( "%c[1B", 27 );
         break;
      case 'r':
         printf( "%c[1C", 27 );
         break;
      case 'l':
         printf( "%c[1D", 27 );
         break;
      };
      fflush( stdout );
   };
   tcsetattr( 0, TCSAFLUSH, &savetty ); // восстановили состояние терминала
   printf( "\n" );
   exit( EXIT_SUCCESS );
}

Хотя этого и не было показано в листинге, но обязательным компонентом подобных программ является перехват сигналов завершения (SIGINT, SIGTERM) для восстановления канонического режима ввода перед завершением программы. В противном случае терминал станет непригоден для дальнейшего использования. Для восстановления режима с успехом может быть использован вызов atexit(), как это сделано в листинге 5.

Листинг 5. Восстановление канонического режима ввода (файл ncan.c)
...
struct termios saved_attributes;
void reset_input_mode( void ) {
   tcsetattr( STDIN_FILENO, TCSANOW, &saved_attributes);
}

void set_input_mode( void ) {
   struct termios tattr;
   ...
   tcgetattr( STDIN_FILENO, &saved_attributes );
   atexit( reset_input_mode );
   ...
   tcsetattr( STDIN_FILENO, TCSAFLUSH, &tattr );
}
...

Заключение

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

Мы завершаем этот цикл статей, который был призван помочь программистам, ранее работавшим с ОС семейства Microsoft Windows при переходе на платформу Linux. Хотя в процессе перехода и могут встретиться незнакомые понятия или нетривиальные решения, но автор надеется, что данный обзорный цикл поможет смягчить трудности "переходного периода" и открыть дверь в богатую вселенную POSIX API.


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


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=969427
ArticleTitle=Инструменты ОС Linux для разработчиков приложений для ОС Windows. Часть 17. Расширенные операции ввода-вывода
publish-date=04242014