Содержание


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

Comments

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

Создание процессов

Для всех операционных систем семейства UNIX/POSIX классическим способом создания параллельного процесса является вызов fork(), который определен в файле <unistd.h>. В листинге 1 представлен простейший пример создания паралелльных процессов в ОС UNIX, полный код которого можно найти в архиве fork.tgz в разделе "Материалы для скачивания". В этом и последующих примерах все вызовы, относящиеся к данной группе API, будут выделены в коде жирным шрифтом.

Листинг 1. Создание процессов (файл p2.c)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include "libdiag.h"
int main( int argc, char *argv[] ) {
   long cali = calibr( 1000 );
   uint64_t t = rdtsc();
   pid_t pid = fork();       // процесс разветвился
   t = rdtsc() - t;
   t -= cali;
   if( pid == -1 ) perror( "fork" ), exit( EXIT_FAILURE );
   if( pid == 0 ) {
      printf( "child with PID=%d finished, start delayed %lu cycles\n", getpid(), t );
      exit( EXIT_SUCCESS );
   }
   if( pid > 0 ) {
      int status;
      wait( &status );
      printf( "parent with PID=%d finished, start delayed %lu cycles\n", getpid(), t );
      exit( EXIT_SUCCESS );
   };
};

Запустим данный пример и изучим полученный результат:

$ ./p2
child with PID=19044 finished, start delayed 855235 cycles
parent with PID=19041 finished, start delayed 109755 cycles
$ ./p2
child with PID=30908 finished, start delayed 166435 cycles
parent with PID=30904 finished, start delayed 106025 cycles

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

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

$ ./p2
child with PID=6172 finished, start delayed 253986 cycles
parent with PID=6171 finished, start delayed 964611 cycles
$ ./p2
child with PID=6174 finished, start delayed 259164 cycles
parent with PID=6173 finished, start delayed 940884 cycles

Запуск этого же теста уже на высокопроизводительном 4-х ядерном процессоре, даёт примерно такой же результат. Порядок временных задержек сохраняется, но порядок активации параллельных процессов может быть произвольным.

$ ./p2
child with PID=26466 finished, start delayed 232627 cycles
parent with PID=26465 finished, start delayed 183480 cycles
$ ./p2
child with PID=26468 finished, start delayed 234885 cycles
parent with PID=26467 finished, start delayed 184555 cycles

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

Листинг 2. Запуск большого числа потоков (файл p4.c)
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main( int argc, char *argv[] ) {
   unsigned long n = 1;
   pid_t pid;
   while( ( pid = fork() ) >= 0 ) {
      if( pid < 0 ) break;
      n++;
      if( pid > 0 ) {
         waitpid( pid, NULL, 0 );
         exit( EXIT_SUCCESS );
      };
   };
   printf( "exit with processes number: %lu\n", n );
   if( pid < 0 ) perror( NULL );
   return 0;
};

Запустим тестовую программу на 2-х процессорном компьютере:

$ time ./p4
exit with processes number: 913
Resource temporarily unavailable
real	0m0.199s
user	0m0.013s
sys	0m0.161s
$ ps -A | wc -l
208

Система смогла одновременно запустить 913 процессов в дополнение к уже существующим в системе.

Теперь выполним этот же пример на 1-но процессорном компьютере (квази-параллельность!) с частотой процессора в 3 раза ниже и объёмом RAM в 4 раза меньше, по сравнению со станцией, использовавшейся при прошлом запуске:

$ time ./p4
exit with processes number: 4028
Resource temporarily unavailable
real	2m59.903s
user	0m0.325s
sys	0m35.891s
$ ps -A | wc -l
109

На данной конфигурации удалось одновременно запустить 4084 дополнительных процесса, но это потребовало в сотни раз больше времени, чем в предыдущем случае. Кроме того, всё это время система была загружена близко к 100% и с большим трудом откликалась на команды с терминала, хотя в ней стационарно выполняется намного меньшее количество процессов.

Во время этого длительного выполнения можно "подсмотреть" состояние таблицы процессов в системе, как показано ниже

$ ps -A
...
 7012 pts/1    00:00:00 p4
 7013 pts/1    00:00:00 p4
 7014 pts/1    00:00:00 p4
 7015 pts/1    00:00:00 p4
 7016 pts/1    00:00:00 p4
 7017 pts/1    00:00:00 p4
...

На этих механизмах, совместно с отображением созданных адресных пространств на исполнимые файлы, базируется основная функциональность для выполнения заданий в ОС UNIX/POSIX/Linux.

Время клонирования

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

Листинг 2. Определение времени создания процесса (файл p2-1.c)
#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>
#include <unistd.h>
#include <sys/wait.h>
#include "libdiag.h"
 
static uint64_t tim;
// размер области данных в пространстве процесса: 1, 10, ... MB
#define data_size 10
#define KB        1024
#define data_byte KB*KB*data_size
static struct mbyte {
#pragma pack( 1 )
   uint8_t array[ data_byte ];
#pragma pack( 4 )
} data;
int main( int argc, char *argv[] ) {
   tim = rdtsc();
   pid_t pid = fork();
   if( pid == -1 ) perror( "fork" ), exit( EXIT_FAILURE );
   if( pid == 0 ) {
      tim = rdtsc() - tim;
      printf( "process create time : %llu\n", tim );
      if( argc > 1 ) {
         long i;
         tim = rdtsc();
         for( i = 0; i < data_byte; i += KB * 4 )
            data.array[ i ] = 0;
         tim = rdtsc() - tim;
         printf( "process write time : %llu\n", tim );
      }
      exit( EXIT_SUCCESS );
   }
   if( pid > 0 ) {
      int status;
      wait( &status );
   };
   exit( EXIT_SUCCESS );
};

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

$ ./p2-1
process create time : 348140
$ ./p2-1
process create time : 326090
$ ./p2-1
process create time : 216020

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

Теперь выполним этот же тестовый пример, но с модификацией страниц памяти (параметр запуска), когда значительная область данных процесса прописывается значением (только 1-й байт каждой 4KБ страницы). Размер области данных при этом устанавливается равным 1 МБ.

$ ./p2-1 w
process create time : 490670
process write time : 1877010
$ ./p2-1 w
process create time : 320200
process write time : 3956830
$ ./p2-1 w
process create time : 1558240
process write time : 2294780

Как можно заметить, для записи 250 байт потребовалось времени на порядок больше, чем на запуск процесса, — это наглядная демонстрация работы механизма COW (copy on write).

Запуск этого же примера, но уже с размером области данных в 10 MБ (для этого потребуется внести изменения в исходный код).

$ ./p2-1 w
process create time : 426220
process write time : 26742080
$ ./p2-1 w
process create time : 166930
process write time : 18489920
$ ./p2-1 w
process create time : 479890
process write time : 31890280

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

Загрузка нового экземпляра процесса

При вызове fork() для нового процесса выделяется собственное адресное пространство, и то, что до выполнения записи пространства 2-х процессов могут перекрываться в силу работы механизма "копирование при записи", принципиально не меняет картину. И только после этого, при необходимости, вновь созданное адресное пространство может быть отображено на исполнимый файл, что можно толковать как загрузка новой задачи в адресное пространство. Для выполнения этой операции POSIX API предлагает целое семейство библиотечных вызовов:

$ man 3 exec
EXEC(3)                    Linux Programmer’s Manual                   EXEC(3)
NAME
       execl, execlp, execle, execv, execvp - execute a file

SYNOPSIS
       #include <unistd.h>
...

Но все они служат оболочками для одного единственного системного вызова:

$ man 2 execve
EXECVE(2)               Руководство программиста Linux               EXECVE(2)
ИМЯ
       execve - выполнить программу
ОБЗОР
       #include <unistd.h>
       int execve(const char *filename, char *const argv [], char *const envp[]);
...

В POSIX API наряду с семейством функций exec*(), которые загружают новые процессы после вызова fork(), представлены и упрощённые механизмы для запуска новых процессов через новый экземпляр командного интерпретатора: system(), popen() и т.д.

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

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

$ sox
sox: SoX v14.2.0
...
$ sudo yum install ogg*
...
$ sudo yum install speex*
...
$ speexdec
Usage: speexdec [options] input_file.spx [output_file]
Decodes a Speex file and produce a WAV file or raw file
...

В архив fork.tgz с примерами включены два файла с образцами звуков — фрагментами женской и мужской речи, позаимствованные из проекта speex. Проверить их звучание и работоспособность аудиопакетов можно утилитой play из состава пакета sox:

$ ls *.wav
female.wav  male.wav
$ play -q male.wav

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

Листинг 3. Воспроизведение файлов в отдельном процессе (файл s5.c)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main( int argc, char *argv[] ) {
   double stret = 1.0;
   int debug_level = 0;
   int c;
   while( -1 != ( c = getopt( argc, argv, "hvs:" ) ) )
      switch( c ) {
         case 's':
            if( 0.0 != atof( optarg ) ) stret = atof( optarg );
            break;
         case 'v': debug_level++; break;
         case 'h':
         default :
            fprintf( stdout,
                     "Опции:\n"
                     " -s - вещественный коэффициент темпо-коррекции\n"
                     " -v - увеличить уровень детализации отладочного вывода\n"
                     " -h - вот этот текст подсказки\n" );
            exit( 'h' == c ? EXIT_SUCCESS : EXIT_FAILURE );
      }
   if( optind == argc )
      fprintf( stdout, "должен быть указан хотя бы один звуковой файл\n" ),
      exit( EXIT_FAILURE );
   char stretch[ 80 ] = "";
   if( 1.0 != stret ) sprintf( stretch, " stretch %f", stret );
   else sprintf( stretch, "" );
   const char *outcmd = "sox%s -twav %s -t alsa default %s";
   int i;
   for( i = optind; i < argc; i++ ) {
      char cmd[ 120 ] = "";
      sprintf( cmd, outcmd,
               0 == debug_level ? " -q" : debug_level > 1 ? " -V" : "",
               argv[ i ],
               stretch );
      if( debug_level > 1 ) fprintf( stdout, "%s\n", cmd );
      system( cmd );
   }
   return EXIT_SUCCESS;
};

Проверим работоспособность данного примера:

$ ./s5 -h
Опции:
 -s - вещественный коэффициент темпо-коррекции
 -v - увеличить уровень детализации отладочного вывода
 -h - вот этот текст подсказки
$ ./s5 male.wav female.wav
$ ./s5 male.wav female.wav -s 0.7
$ ps -Af | tail -n10
...
olej     10034  7176  0 14:07 pts/10   00:00:00 ./s5 male.wav female.wav -s 2
olej     10035 10034  0 14:07 pts/10   00:00:00 sox -q -twav male.wav -t alsa ...
...

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

В следующем подобном примере для создания входного и выходного потоков используются вызовы popen(). Посредством popen() программа запускает два дочерних процесса-фильтра, и в итоге получается 3 работающих процесса. Входной процесс трансформирует несколько предусмотренных входных форматов (RAW, WAV, Vorbis, Speex) в единый "необработанный" поток отсчётов RAW, головная программа считывает этот поток поблочно (размер блока можно менять) и передаёт эти блоки в темпе считывания выходному дочернему процессу, который, используя sox, воспроизводит этот поток (возможно делая для него темпо-коррекцию).

При таком алгоритме каждый отсчёт аудио потока будет последовательно "протекать" через цикл головного процесса, и в этой точке к потоку могут быть применены любые дополнительные алгоритмы цифровой обработки сигнала. Но прежде, чем испытывать программу, необходимо подготовить входной тестовый файл, в качестве которого создадим сжатый speex файл:

$ speexenc male.wav male.spx
Encoding 8000 Hz audio using narrowband mode (mono)
$ ls -l male.*
-rw-rw-r-- 1 olej olej 11989 Май 12 13:47 male.spx
-rw-r--r-- 1 olej olej 96044 Авг 21  2008 male.wav

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

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

Листинг 4. Преобразование звуковых файлов (файл o5.c)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
static int debug_level = 0;
static char stretch[ 80 ] = "";
u_long buflen = 1024;
u_char *buf;
// конвейер, которым мы читаем RAW файлы (PCM 16-бит), пример:
// $ cat male.raw | sox -u -s -b16 -r8000 -traw - -t alsa
// конвейер, которым мы читаем WAV файлы (или OGG Vorbis), пример:
// $ sox -V male.wav -traw -u -sw - | sox -u -s -b16 -r8000 -traw - -t alsa
// конвейер, которым мы читаем OGG SPEEX файлы, пример:
// $ speexdec -V male.spx - | sox -u -s -b16 -r8000 -traw - -t alsa
void play_file( char *filename ) {
   struct stat sbuf;
   if( stat( filename, &sbuf ) < 0 ) {
      fprintf( stdout, "неверное имя файла: %s\n", filename );
      return;
   }
   // форматы файла различаются по имени, но должны бы ещё и по magic
   // содержимому с начала файла: "RIFF..." - для *.wav, "Ogg ... vorbis" - для
   // *.ogg, "Ogg ... Speex" — для *.spx, или отсутствие magic признаков
   // для *.pcm, *.raw
   const char *ftype[] = { ".raw", ".pcm", ".wav", ".ogg", ".spx" };
   int stype = sizeof( ftype ) / sizeof( ftype[ 0 ] ), i;
   for( i = 0; i < stype; i++ ) {
      char *ext = strstr( filename, ftype[ i ] );
      if( NULL == ext ) continue;
      if( strlen( ext ) == strlen( ftype[ i ] ) ) break;
   }
   if( i == stype ) {
      fprintf( stdout, "неизвестный формат аудио файла: %s\n", filename );
      return;
   };
   char cmd[ 120 ];
   const char *inpcmd[] = {
      "cat %s",
      "sox%s %s -traw -u -s -",
      "speexdec%s %s -"
   };
   const int findex[] = { 0, 0, 1, 1, 2 };
   const char* cmdfmt = inpcmd[ findex[ i ] ];
   if( 0 == findex[ i ] )
      sprintf( cmd, cmdfmt, filename );
   else if( 1 == findex[ i ] )
      sprintf( cmd, cmdfmt,
               0 == debug_level ? " -q" : debug_level > 1 ? " -V" : "",
               filename, stretch );
   else
      sprintf( cmd, cmdfmt, debug_level > 1 ? " -V" : "", filename );
   if( debug_level > 1 ) fprintf( stdout, "%s\n", cmd );
   FILE *fsox = popen( cmd, "r" );
   const char *outcmd = "sox%s -u -s -b16 -r8000 -traw - -t alsa default %s";
   sprintf( cmd, cmdfmt = outcmd,
            0 == debug_level ? " -q" : debug_level > 1 ? " -V" : "",
            stretch );
   if( debug_level > 1 ) fprintf( stdout, "%s\n", cmd );
   FILE *fplay = popen( cmd, "w" );
   int in, on, s = 0;
   while( in = fread( buf, 1, buflen, fsox ) ) {
      if( debug_level ) fprintf( stdout, "read : %d - ", in ), fflush( stdout );
      on = fwrite( buf, 1, in, fplay );
      if( debug_level ) fprintf( stdout, "write : %d\n", on ), fflush( stdout );
      s += on;
   }
   if( debug_level ) fprintf( stdout, "воспроизведено: %d байт\n", s );
}
int main( int argc, char *argv[] ) {
   int c;
   double stret = 1.0;
   while( -1 != ( c = getopt( argc, argv, "vs:b:" ) ) )
      switch( c ) {
         case 's':
            if( 0.0 != atof( optarg ) ) stret = atof( optarg );
            break;
         case 'b': if( 0 != atol( optarg ) ) buflen = atol( optarg ); break;
         case 'v': debug_level++; break;
         case 'h':
         default :
            fprintf( stdout,
                     "Опции:\n"
                     " -s - вещественный коэффициент темпо-коррекции\n"
                     " -b - размер аудио буфера\n"
                     " -v - увеличить уровень детализации отладочного вывода\n"
                     " -h - вот этот текст подсказки\n" );
            exit( 'h' == c ? EXIT_SUCCESS : EXIT_FAILURE );
      }
   if( optind == argc )
      fprintf( stdout, "должен быть указан хотя бы один звуковой файл\n" ),
      exit( EXIT_FAILURE );
   if( 1.0 != stret ) sprintf( stretch, " stretch %f", stret );
   else sprintf( stretch, "" );
   buf = malloc( buflen );
   int i;
   for( i = optind; i < argc; i++ ) play_file( argv[ i ] );
   free( buf );
   return EXIT_SUCCESS;
};

Запустим пример на различных форматах аудиофайлов и сравним время исполнения:

$ time ./o5 -b7000 male.spx
Decoding 8000 Hz audio using narrowband mode (mono)
Encoded with Speex 1.2rc1
real	0m0.093s
user	0m0.000s
sys	0m0.001s
$ time play -q male.wav
real	0m8.337s
user	0m0.009s
sys	0m0.011s

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

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

Данный пример, в отличие от предыдущих, использует язык С++ для того, чтобы изолировать все рутинные действия по созданию дочернего процесса и перехвата его потоков ввода-вывода в отдельный объект класса chld. В этом коде присутствует много возможностей POSIX API, которые уже упоминались ранее: fork(), execvp(), создание неименованных каналов pipe() для связи процессов, переназначение на них потоков ввода/вывода dup2(), неблокирующий ввод устанавливаемый вызовом fcntl() и другие:

Листинг 5. Трансформация звуковых файлов (файл e5.cc)
Листинг 5. Трансформация звуковых файлов (файл e5.cc)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <iostream>
#include <iomanip>
using std::cin;
using std::cout;
using std::endl;
using std::flush;
class chld {
   int fi[ 2 ], // pipe – для ввода в дочернем процессе
       fo[ 2 ]; // pipe – для вывода в дочернем процессеpid_t pid;
   char** create( const char *s );
public:
   chld( const char*, int* fdi, int* fdo );
};
// внутренняя private функция для построения списка параметров запуска процесса
char** chld::create( const char *s ) {
   char *p = (char*)s, *f;
   int n;
   for( n = 0; ; n++ ) {
     if( ( p = strpbrk( p, " " ) ) == NULL ) break;
     while( *p == ' ' ) p++;
   };
   char **pv = new char* [ n + 2 ];
   for( int i = 0; i < n + 2; i++ ) pv[ i ] = NULL;
   p = (char*)s;
   f = strpbrk( p, " " );
   for( n = 0; ; n++ ) {
      int k = ( f - p );
      pv[ n ] = new char[ k + 1 ];
      strncpy( pv[ n ], p, k );
      pv[ n ][ k ] = '\0';
      p = f;
      while( *p == ' ' ) p++;
      if( ( f = strpbrk( p, " " ) ) == NULL ) {
         pv[ n + 1 ] = strdup( p );
         break;
      }
   }
   return pv;
};
// главная функциональность класса – конструктор, здесь переназначаются
// потоки ввода вывода (SYSIN & SYSOUT), клонируется вызывающий процесс,
// и в нём вызывается новый процесс-клиент со своими параметрами:
chld::chld( const char* pr,  int* fdi, int* fdo ) {
   if( pipe( fi ) || pipe( fo ) ) perror( "pipe" ), exit( EXIT_FAILURE );
   // здесь создаётся список параметров запуска
   char **pv = create( pr );
   pid = fork();
   switch( pid ) {
      case -1: perror( "fork" ), exit( EXIT_FAILURE );
      case  0:   // дочерний клон
         close( fi[ 1 ] ), close( fo[ 0 ] );
         if( dup2( fi[ 0 ], STDIN_FILENO ) == -1 ||
             dup2( fo[ 1 ], STDOUT_FILENO ) == -1 )
            perror( "dup2" ), exit( EXIT_FAILURE );
         close( fi[ 0 ] ), close( fo[ 1 ] );
         // запуск консольного клиента
         if( -1 == execvp( pv[ 0 ], pv ) )
            perror( "execvp" ), exit( EXIT_FAILURE );
         break;
      default:   // родительский процесс
         for( int i = 0;; i++ )
            if( pv[ i ] != NULL ) delete pv[ i ]; else break;
         delete [] pv;
         close( fi[ 0 ] ), close( fo[ 1 ] );
         *fdi = fo[ 0 ];
         int cur_flg;
         // чтение из родительского процесса должно быть в режиме O_NONBLOCK
         cur_flg = fcntl( fo[ 0 ], F_GETFL );
         if( -1 == fcntl( fo[ 0 ], F_SETFL, cur_flg | O_NONBLOCK ) )
            perror( "fcntl" ), exit( EXIT_FAILURE );
         *fdo = fi[ 1 ];
         // для записи O_NONBLOCK не обязательно
         break;
   };
}; // конец определения класса chld

static int debug_level = 0;
static u_long buflen = 1024;
static u_char *buf;
static char stretch[ 80 ] = "";
int main( int argc, char *argv[] ) {
   int c;
   double stret = 1.0;
   while( -1 != ( c = getopt( argc, argv, "vs:b:" ) ) )
      switch( c ) {
         case 's':
            if( 0.0 != atof( optarg ) ) stret = atof( optarg );
            break;
         case 'b': if( 0 != atol( optarg ) ) buflen = atol( optarg ); break;
         case 'v': debug_level++; break;
         case 'h':
         default : cout <<
                     argv[ 0 ] << "[<опции>] <имя вх.файла> <имя вых.файла>\n"
                     "опции:\n"
                     " -s - вещественный коэффициент темпо-коррекции\n"
                     " -b - размер аудио буфера\n"
                     " -v - увеличить уровень детализации отладочного вывода\n"
                     " -h — вот этот текст подсказки\n";
            exit( 'h' == c ? EXIT_SUCCESS : EXIT_FAILURE );
      }
   if( optind != argc - 2 )
      cout << "должно быть указаны имена входного и выходного звуковых файлов"
           << endl, exit( EXIT_FAILURE );
   // файл с которого читается входной аудио-поток
   int fai = open( argv[ optind ], O_RDONLY );
   if( -1 == fai ) perror( "open input" ), exit( EXIT_FAILURE );
   // файл в который пишется результирующий аудио-поток
   int fao = open( argv[ optind + 1 ], O_RDWR | O_CREAT,  // 666
                   S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH |  S_IWOTH );
   if( -1 == fao ) perror( "open output" ), exit( EXIT_FAILURE );
   char stretch[ 80 ] = "";
   if( 1.0 != stret ) sprintf( stretch, " stretch %f", stret );
      else sprintf( stretch, "" );
   char comstr[ 120 ] = "sox -V -twav - -twav - ";
   strcat( comstr, stretch );
   // сформирована командная строка дочернего процесса
   if( debug_level > 1 ) cout << comstr << endl;
   int fdi, fdo;
   chld *ch = new chld( comstr, &fdi, &fdo );
   // дескриптор с которого читается вывод в stdout дочернего процесса
   if( -1 == fdi ) perror( "pipe output" ), exit( EXIT_FAILURE );
   // дескриптор куда записывается то, что читает из stdin дочерний процесс
   if( -1 == fdo ) perror( "pipe output" ), exit( EXIT_FAILURE );
   buf = new u_char[ buflen ];
   int sum[] = { 0, 0, 0, 0 };
   while( true ) {
      int n;
      if( fai > 0 ) {
         n = read( fai, buf, buflen );
         sum[ 0 ] += n > 0 ? n : 0;
         if( debug_level > 2 )
            cout << "READ from audio\t" << n << " -> " << sum[ 0 ] << endl;
         if( -1 == n ) perror( "read file" ), exit( EXIT_FAILURE );
         if( 0 == n ) close( fai ), fai = -1;
      };
      if( fai > 0 ) {
         n = write( fdo, buf, n );
         sum[ 1 ] += n > 0 ? n : 0;
         if( debug_level > 2 )
            cout << "WRITE to child\t" << n << " -> "
                 << ( sum[ 1 ] += n > 0 ? n : 0 ) << endl;
         if( -1 == n ) perror( "write pipe" ), exit( EXIT_FAILURE );
         // дополнительное время на обработку
         usleep( 100 );
      }
      else close( fdo ), fdo = -1;
      n = read( fdi, buf, buflen );
      if( debug_level > 2 )
         cout << "READ from child\t" << n << " -> "
              << ( sum[ 2 ] += n > 0 ? n : 0 ) << flush;
      if( n >= 0 && debug_level > 2 ) cout << endl;
      // это может быть только после закрытия fdo!!!
      if( 0 == n ) break;
      else if( -1 == n ) {
         if( EAGAIN == errno ) {
            if( debug_level > 2 )
               cout << " : == not ready == ... wait ..." << endl;
            usleep( 300 );
            continue;
         }
         else perror( "\nread pipe" ), exit( EXIT_FAILURE );
      }
      n = write( fao, buf, n );
      if( debug_level > 2 )
         cout << "WRITE to file\t" << n << " -> "
              << ( sum[ 3 ] += n > 0 ? n : 0 ) << endl;
      if( -1 == n ) perror( "write file" ), exit( EXIT_FAILURE );
   };
   close( fai ), close( fao );
   close( fdi ), close( fdo );
   delete [] buf;
   delete ch;
   cout << "считано со входа " << sum[ 0 ] << " байт - записано на выход "
        << sum[ 0 ] << " байт" << endl;
   return EXIT_SUCCESS;
};

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

$ ./e5 -vvv -s0.5 -b7000 male.wav male1.wav
sox -V -twav - -twav -  stretch 0.500000
READ from audio>7000 -> 7000
WRITE to child<>7000 -> 14000
READ from child>-1 -> 0 : == not ready == ... wait ...
READ from audio>7000 -> 14000
WRITE to child<>7000 -> 28000
READ from child>-1 -> 0 : == not ready == ... wait ...
READ from audio>7000 -> 21000
WRITE to child<>7000 -> 42000
READ from child>-1 -> 0 : == not ready == ... wait ...
READ from audio>7000 -> 28000
WRITE to child<>7000 -> 56000
...

В выводе видны строки неблокирующего вывода, когда данные ещё не готовы

(== not ready == ... wait ... ).

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

$ play male1.wav
...  
$ ls -l male*.wav
-rw-rw-r-- 1 olej olej 48044 Май 12 19:58 male1.wav
-rw-r--r-- 1 olej olej 96044 Авг 21  2008 male.wav

Как и ожидалось, результирующий файл male1.wav является копией исходного male.wav (по содержимому), но с темпокоррекцией в 2 раза в сторону ускорения (число отсчётов и размер файла уменьшились вдвое).

Заключение

В этой статье мы рассмотрели возможности API POSIX, предназначенные для работы с процессами, и заодно познакомились с различными пакетами для работы со звуковыми форматами.

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


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


Похожие темы


Комментарии

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

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