Библиотека сайта rus-linux.net
Цилюрик О.И. Linux-инструменты для Windows-программистов | ||
Назад | Библиотеки API POSIX | Вперед |
Загрузка нового экземпляра (процесса)
Вызов fork()
создаёт новое адресное пространство процесса (то, что до выполнения
записи пространства 2-х процессов могут перекрываться в силу работы
механизма «копирование при записи» - принципиально не
меняет картину). Только после этого, если это необходимо, это вновь
созданное адресное пространство может быть отображено на исполнимый
файл (что можно толковать как загрузка новой задачи в адресное
пространство). Выполняется это действие целым семейством библиотечных вызовов :
$ man 3 exec EXEC(3) Linux Programmer’s Manual EXEC(3) NAME execl, execlp, execle, execv, execvp - execute a file SYNOPSIS #include <unistd.h> extern char **environ; int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); ...
Но все они являются обёртками для единого системного вызова :
$ man 2 execve EXECVE(2) Руководство программиста Linux EXECVE(2) ИМЯ execve - выполнить программу ОБЗОР #include <unistd.h> int execve(const char *filename, char *const argv [], char *const envp[]); ...
Но кроме целого семейства функций exec*()
, после fork()
загружающие новые процессы, предоставляются ещё упрощённые механизмы
запуска новых процессов через новый экземпляр командного
интерпретатора: system(), popen()
, ... Ниже различные способы-мехаизмы
рассматриваются на сравнительных примерах. Для того, чтобы лучше
оценить мощь механизмов POSIX, в примерах будет использоваться не
перенаправление символьной информации в потоках (что достаточно
привычно, например, из использования конвейеров консольных команд), а
потоков аудиоинформации и использование дочерних процессов из пакетов
процесса sox, ogg, speex
(что достаточно необычно).
Примечание: Проверьте прежде наличие в вашей
системе этих установленных пакетов, это хотя все и широко
распространённые пакеты, но они не является составной частью
дистрибутива, и может потребовать дополнительной установки с помощью
пакетного менеджера yum
:
$ sox sox: SoX v14.2.0 ... $ sudo yum install ogg* ... $ ls /usr/bin/ogg* /usr/bin/ogg123 /usr/bin/oggCut /usr/bin/oggenc /usr/bin/oggLength /usr/bin/oggSlideshow /usr/bin/oggCat /usr/bin/oggdec /usr/bin/ogginfo /usr/bin/oggResize /usr/bin/oggSplit /usr/bin/oggconvert /usr/bin/oggDump /usr/bin/oggJoin /usr/bin/oggScroll /usr/bin/oggThumb $ 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
):
$ ls *.wav female.wav male.wav
Проверить их звучание, и работоспособность аудиопакетов, можно утилитой play из состава пакета sox:
$ play -q male.wav
Теперь мы готовы вернуться к сравнительным примерам
запуска дочерних процессов трансформации и воспроизведения
аудиопотоков. Первый пример простейшим образом запускает вызовом system()
программу sox
в качестве дочернего процесса, для воспроизведения списка файлов,
заданных в качестве параметров строки, с возможностью изменения
темпо-ритма воспроизведения без искажения тембра:
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 должен быть указан хотя бы один звуковой файл $ ./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 default stretch 2.000000 ...
Этот пример я представляю ещё и для того, чтобы
остановить внимание на том факте, что и простейшего вызова 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 раз без потери качества, варьируя
параметрами speexenc
можно это сжатие сделать ещё больше.
Теперь собственно сам пример (пример великоват, но он стоит того, чтобы с ним поэкспериментировать):
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 default stretch 0.9 // конвейер, которым мы читаем WAV файлы (или OGG Vorbis), пример: // $ sox -V male.wav -traw -u -sw - | sox -u -s -b16 -r8000 -traw - -t alsa default stretch 0.9 // конвейер, которым мы читаем OGG SPEEX файлы, пример: // $ speexdec -V male.spx - | sox -u -s -b16 -r8000 -traw - -t alsa default stretch 0.9 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; };
Исполнение примера на различных форматах аудиофайлов:
$ ./o5 male.wav
$ ./o5 male.raw
Интересно сравнить времена исполнения:
$ 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()
и другие:
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; };
Детали и опциональные ключи программы оставим для экспериментов, покажем только как программа трансформирует аудио файл в другой аудио файл, с темпо-ритмом увеличенным вдвое (установлен максимальный уровень детализации диагностического вывода, показано только начало вывода диагностики):
$ ./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
является копией исходного (по содержимому), с темпокоррекцией в 2
раза в сторону ускорения (число отсчётов и размер файла уменьшились
вдвое).
Предыдущий раздел: | Оглавление | Следующий раздел: |
Время клонирования | Сигналы |