Рейтинг@Mail.ru

Наши друзья и партнеры

купить дешевый 
компьютер родом из Dhgate.com




Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

На главную -> MyLDP -> Электронные книги по ОС Linux
Цилюрик О.И. 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 раза в сторону ускорения (число отсчётов и размер файла уменьшились вдвое).


Предыдущий раздел: Оглавление Следующий раздел:
Время клонирования   Сигналы

Поделиться: