IPC в Unix-системах

Андрей Боровский

kylixportal@narod.ru

Термином IPC (inter-process communication), который обычно переводится как “межпроцессное взаимодействие”, обозначается передача данных между процессами в рамках одной системы. Механизмы взаимодействия, рассмотренные в этой статье, реализованы практически одинаково во всех современных Unix-системах (Solaris, Linux, FreeBSD и т. п.). По сравнению с другими ОС Unix-системы обладают весьма богатым набором механизмов передачи данных. Отчасти это объясняется тем, что средства, изначально разрабатывавшиеся в разных ветвях Unix, внедрялись затем в другие версии в целях обеспечения совместимости.

Далее будут рассмотрены такие средства взаимодействия между процессами как однонаправленные каналы, сокеты, сообщения, разделяемые блоки памяти и семафоры. В этой статье не рассматриваются буферы обмена и Drag and Drop, так как в Unix-системах эти механизмы обычно реализованы на уровне графической оболочки, и их реализация зависит от того, какая оболочка используется.

Однонаправленные каналы

Однонаправленные каналы (pipes) позволяют передавать данные только в одном направлении. На уровне интерфейса программирования канал представляется двумя дескрипторами файлов, один из которых служит для чтения данных, а другой – для записи. Каналы не поддерживают произвольный доступ, т. е. данные из канала могут считываться только в том же порядке, в котором они записывались. Однонаправленные каналы могут быть анонимными (anonymous pipes) и именованными (named pipes, FIFOs).

Анонимные каналы

Анонимные каналы отличаются тем, что оба файловых дескриптора открываются в контексте одного и того же процесса. Обычно такие каналы используются совместно с функцией fork и служат для передачи данных между двумя копиями процесса, полученными при вызове этой функции. Анонимный однонаправленный канал создается при помощи функции pipe (объявляется в файле unistd.h). В качестве параметра функции pipe передается массив типа int, состоящий из двух элементов. В первом элементе массива функция возвращает дескриптор файла, служащий для чтения данных, а во втором – дескриптор для записи. С полученными дескрипторами можно обращаться также, как и с обычными дескрипторами файлов. Ниже приводится пример использования функции pipe.

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
int main () {
int pipedes[2];
int pid;
char *str = "String passed via pipe";
FILE *f;
pipe(pipe
des); // создаем канал
/* дескриптор pipedes[0] открыт для чтения, а pipedes[1] – для записи */
pid = fork(); // создаем два процесса
if ( pid > 0 ) { // родительский процесс
write(pipedes[1], (void *) str, strlen(str) + 1);
close(pipedes[1]);
return EXI
T_SUCCESS;
} else { // дочерний процесс
f = fdopen(pipedes[0], "r");
while ( putchar(getc(f)) != '\0' );
putchar('\n');
fclose(f);
return EXIT_SUCCESS;
}
}

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

Именованные каналы

В отличие от анонимных каналов именованные однонаправленные каналы позволяют передавать данные между независимыми процессами. Для идентификации именованного канала на диске создается файл специального типа pipe. Следует помнить о том, что файлы именованных каналов являются средством передачи, а не хранения, данных. В частности, размер файла канала всегда остается равным нулю. Для файлов именованных каналов действуют те же правила контроля доступа, что и для обычных файлов Unix. Для создания именованных каналов служит функция mkfifo. Первый параметр этой функции – строка, в которой передается имя файла, идентифицирующего канал, второй параметр – маска прав доступа к файлу. Функция mkfifo вызывается для создания файла только один раз. После создания файла канала оба приложения, участвующие в обмене данными, должны открыть этот файл. Одно приложение открывает файл канала для записи, другое для чтения. Далее следует пример программы, которая создает файл канала, открывает его для записи и записывает в него строку.

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
int main () {
FILE * f;
char * str = "String passed
via FIFO";
char * fname = "~/fifo";
mkfifo(fname, 010777); // создаем файл канала
f = fopen(fname, "w"); // открываем файл для записи
fwrite((void*) str, strlen(str) + 1, 1, f);
fclose(f);
return EXIT_SUCCESS;
}

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

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE * f;
char * fname = "~/fifo";
f = fopen(fname, "r"); // открываем файл для чтения
while ( putchar(getc(f)) != '\0' );
putchar
('\n');
fclose(f);
return EXIT_SUCCESS;
}

Программу, создающую файл канала, следует запускать первой. Следует отметить, что система синхронизирует процессы, связанные с каналами. Например, при вызове функции чтения или записи в канал выполнение процесса приостанавливается до тех пор, пока “на другом конце” канала не будет выполнена комплиментарная операция.

Функции popen и pclose

Среди средств работы с каналами особого внимания заслуживает функция popen. Эта функция запускает внешнюю программу и возвращает вызвавшему ее приложению указатель, связанный либо со стандартным потоком ввода, либо со стандартным потоком вывода запущенного процесса. Первый параметр функции popen – строка, содержащая команду, запускающую внешнюю программу. Второй параметр определяет, какой из стандартных потоков (вывода или ввода) будет возвращен. Аргумент “w” соответствует потоку ввода запускаемой программы, в этом случае приложение, вызвавшее popen записывает данные в поток. Аргумент “r” соответствует потоку вывода. В следующем примере функция popen используется для запуска команды ls , так что данные, выданные этой командой направляются приложению, вызвавшему popen:

f = popen("ls /usr/bin", "r");
act_size = fread(buf, 1, buf_size, f);
...
pclose(f);

Файлы сокетов и каналов в окне Konqueror

Сокеты

Сокеты, впервые реализованные в BSD Unix, поддерживаются всеми современными операционными системами. Сокеты являются основой сетевых приложений, однако в Unix-системах они могут использоваться также и для обмена данными между локальными процессами. Следует уточнить, что речь здесь идет именно о специальных “несетевых” сокетах, так как локальные приложения, естественно, могут обмениваться данными, используя сетевые протоколы.

Сокеты в файловом пространстве имен

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

Для работы с адресами из файлового пространства используется специальная структура данных sockaddr_un, определенная в файле sys/un.h. В следующем фрагменте кода сокет связывается с файлом.

#include <sys/socket.h>
#include <sys/un.h>
...
struct sockaddr_un name;
int sock, size;
char * fn = "/tmp/filesocket";
sock = socket(PF_FILE, SOCK_DGRAM, 0);
name.sun_family = AF_FILE;
strcpy(name.sun_path, fn);
size = offsetof(struct sockaddr_un, sun_path) + strlen(name.sun_path) + 1;
bind(sock, (struct sockaddr *) &name, size);
...

В результате выполнения приведенного фрагмента на диске будет создан файл /tmp/filesocket. По окончании работы с сокетом этот файл должен быть удален. Значение, возвращенное функцией socket является файловым дескриптором и может быть использовано, например, для чтения данных:

act_size = read ( sock, buf, buf_size );

Приложение-клиент может соединиться с уже созданным сокетом при помощи функции connect:

#include <sys/socket.h>
#include <sys/un.h>
...
struct sockaddr_un name;
int sock;
char * fn = "/tmp/filesocket";
char * greeting = "How're you?";
sock = socket(PF_FILE, SOCK_DGRAM, 0);
name.sun_family = AF_FILE;
strcpy(name.sun_path, fn);
connect(sock, (struct sockaddr *) &name, sizeof(name));
write(sock, greeting, strlen(greeting));
...

Связанные сокеты

Связанные сокеты (socket pairs) похожи на анонимные однонаправленные каналы. Разница между сокетами и однонаправленными каналами заключается в том, что сокеты могут передавать данные в обоих направлениях. Связанные сокеты создаются при помощи функции socketpair. Как и функция pipe, функция socketpair возвращает два файловых дескриптора, но, в отличие от pipe, оба эти дескриптора открыты и для записи, и для чтения. Далее следует программа, подобная приведенному выше примеру использования функции pipe.

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <strings.h>
#define MAXLEN 512
int main () {
int sockets[2];
int size, pid;
char * greeting = "How are you?";
char * response = "I'm ok, thanks!";
char buf[MAXLEN];
socketpair(AF_FILE, SO
CK_DGRAM, 0, sockets) // создаем пару сокетов
pid = fork(); // создаем два процесса из одного
switch ( pid ) {
case 0: // дочерний процесс
write(sockets[0], greeting, strlen(greeting) + 1);
size = read(sockets[0], buf, MAXLEN);
if ( size = MAXLEN ) buf[MA
XLEN – 1] = '\0';
printf("Parent responded: %s\n", buf);
close(sockets[0]);
return EXIT_SUCCESS;
case -1: // ошибка при вызове fork
perror("fork");
return EXIT_FAILURE;
default: // родительский процесс
size = read(sockets[1], buf, MAXLEN);
printf("Child se
nt: %s\n", buf);
write(sockets[1], response, strlen(response) + 1);
close(sockets[1]);
return EXIT_SUCCESS;
}
}

Сообщения

Механизм обмена данными при помощи сообщений был стандартизирован в рамках System V Interface Definition (SVID) и в настоящее время поддерживается всеми основными Unix-системами (проверить, реализованы ли в конкретной системе механизмы обмена данными SVID можно при помощи команды ipcs). Сообщение определяется как “последовательность байтов, передаваемая от одного процесса другому”. Система сообщений SVID обладает следующими свойствами:
Возможность накопления сообщений в очереди. Следует отметить, что в Unix не существует системной очереди сообщений. Приложения, использующие сообщения для обмена данными, создают свою собственную очередь сообщений, которая может (и должна) быть удалена приложением-владельцем в момент завершения его работы.
Возможность произвольного выбора сообщений из очереди на основе назначенных им идентификаторов. Эта возможность позволяет организовать приоритетную обработку сообщений, а также идентифицировать сообщения, посылаемые разными приложениями, участвующими в обмене данными.
Произвольная структура и размер сообщения. Сообщения могут служить для передачи любых объемов данных (хотя использовать их для передачи очень
больших объемов данных не рекомендуется). Система предъявляет минимальные требования к структуре сообщения.

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

struct msgp {
long mtype;
char mtext[SOMEVALUE];
};

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

Все типы, константы и функции, использующиеся при работе с сообщениями объявлены в файлах sys/ipc.h и sys/msg.h. Очередь сообщений создается при помощи функции msgget. Первый параметр этой функции – ключ – уникальное число, идентифицирующее очередь. Для получения таких чисел можно использовать функцию ftok, однако руководство по работе с функциями SVID рекомендует выбирать значения самостоятельно. Второй параметр функции msgget представляет собой маску прав доступа к создаваемой очереди (аналогичную маске прав доступа к файлам) и несколько дополнительных флагов. Флаг IPC_CREATE указывает, что в результате вызова msgget должна быть создана новая очередь. При установке флага IPC_EXCL, функция msgget вернет сообщение об ошибке, если очередь с указанным ключом уже существует.

Передача и получение сообщений выполняется при помощи функций msgsnd и msgrcv соответственно. Первым параметром обеих функций является идентификатор очереди, возвращенный функцией msgget. Во втором параметре передается размер структуры сообщения. Программа, читающая сообщения из очереди, должна определять размер сообщения на основе его идентификатора. Третьим параметром функции msgrcv является идентификатор сообщения. Если значение этого параметра больше нуля, из очереди будет извлечено сообщение с соответствующим значением идентификатора. Если этот параметр равен нулю, из очереди будет извлечено любое первое сообщение, а если параметр отрицательный, из очереди будет извлечено первое сообщение, чей идентификатор меньше либо равен абсолютному значению параметра. Последний параметр в обеих функциях позволяет задать дополнительные флаги. Обычно функции, работающие с сообщениями, приостанавливают выполнение программы до тех пор, пока помещение сообщения в очередь или извлечение сообщения из нее не будет выполнено. При указании флага IPC_NOWAIT функция вернет сообщение об ошибке, если соответствующая операция не может быть выполнена немедленно.

Функция msgctl позволяет управлять очередью и получать данные о ее состоянии. Например, вызов

msgctl(msgid, IPC_RMID, 0);

где msgid – идентификатор очереди сообщений, возвращенный функцией msgget, удаляет ранее созданную очередь.

В качестве примера рассмотрим две программы. Одна программа (сервер) создает очередь сообщений. Другая программа (клиент) использует эту очередь для отправки сообщений серверу и получения ответа. Программа-сервер выглядит следующим образом:

#include <stdio.h>
#include <stdli
b.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <strings.h>
#define KEY 1174 // "магическое" число
#define MAXLEN 512
struct msg_t {
long mtype;
int snd_pid;
char body[MAXLEN];
};
int main() {
struct msg_t message;
int msgi
d;
char * response = "Ok!";
msgid = msgget(KEY, 0777 | IPC_CREAT); // создаем очередь сообщений
msgrcv(msgid, &message, sizeof(message), 2, 0); // ждем сообщение
printf("Client (pid = %i) sent: %s", message.snd_pid, message.body);
message.mtype = 1;
messag
e.snd_pid = getpid();
strcpy(message.body, response);
msgsnd(msgid, &message, sizeof(message), 0); // посылаем ответ
msgrcv(msgid, &message, sizeof(message), 2, 0); // ждем подтверждения
msgctl(msgid, IPC_RMID, 0); // удаляем очередь
return EXIT_SUCCESS;
}

Ниже приводится исходный текст программы-клиента

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <strings.h>
#define KEY 1174 // "магическое" число
#define MAXLEN 512
struct msg_t {
long mtype;
int snd_pid;
char body[MAXLEN];
};
int main() {
int msgid;
int i;
struct msg_t message;
char buf[MAXLEN];
msgid = msgget(KEY, 0777); // получаем идентификатор очереди
i = 0;
while ( (i < (MAXLEN - 1)) && ((message.body[i++] = getchar()) != '\n') );
me
ssage.body[i] = '\0';
message.mtype = 2;
message.snd_pid = getpid ();
msgsnd(msgid, &message, sizeof(message), 0); // посылаем сообщение
msgrcv(msgid, &message, sizeof(message), 1, 0); // ждем ответа
printf("Server (pid= %i) responded: %s\n", message.snd_p
id, message.body);
message.mtype = 2;
msgsnd(msgid, &message, sizeof(message), 0); // посылаем подтверждение
return EXIT_SUCCESS;
}

Сервер и клиент используют разные идентификаторы для посылаемых сообщений. Это сделано для того, чтобы программа, вызывающая последовательно msgsnd и msgrcv, не извлекала из очереди свои собственные сообщения. Программу-сервер следует запустить до запуска программы-клиента. Запустив из другого окна консоли программу-клиент, наберите небольшой текст и нажмите ввод.

Разделяемая память

Спецификация SVID описывает интерфейс для работы с разделяемыми блоками памяти. Разделяемые блоки памяти представляют собой область памяти, отображенную адресное пространство нескольких процессов. Функции для работы с разделяемой памятью объявлены в файлах sys/ipc.h sys/shm.h.

Разделяемый блок памяти выделяется при помощи функции shmget, которой передаются три параметра. В первом параметре передается ключ, идентифицирующий выделяемый блок памяти. Второй параметр позволяет указать размер блока памяти (в байтах). В третьем параметре передается маска прав доступа и флаги, аналогичные флагам msgget. Функция shmget возвращает идентификатор выделенного блока памяти (его не следует путать с указателем на блок). Отображение блока памяти в адресное пространство процесса выполняет функция shmat. У функции shmat три параметра. Первый параметр, это идентификатор, возвращенный функцией shmget. Во втором параметре передается желательный адрес начала области отображения блока. Функция shmat “постарается” отобразить блок в указанную область, хотя успешный результат негарантирован. Если в этом параметре передать нулевое значение, функция сама выберет начальный адрес области отображения. Третий параметр позволяет задать дополнительные флаги. Например, флаг SHM_RDONLY присваивает отображаемой области статус “только для чтения”. При успешном выполнении функция shmat возвращает указатель на начало области отображения. Функция shmdt удаляет область отображения (но не блок разделяемой памяти). Единственный параметр этой функции – указатель, возвращенный функцией shmat. Для удаления блока разделяемой памяти нужно вызвать функцию shmctl:

shmctl(shmid, IPC_RMID, 0);

где shmid – идентификатор, возвращенный функцией shmget.

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

Программа-сервер:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#define MAXLEN 512
struct shmem_t {
int locked;
char string[MAXLEN];
};
int main() {
key_t key;
int shmid;
struct shmem_t * shared_area;
key = ftok("/home/andrei/.bashrc", 1); // генерация ключа
/* выделяем блок разделяемой памяти */
shmid = shmget(key, sizeof(struct shmem_t), 0777 | IPC_CREAT);
/* отображаем блок разделяемой памяти в адресное пространство процесса */
shared_area = (struct shmem_t *) shmat(shmid, 0, 0);
shared_area->locked = 0;
while ( shared_area->locked == 0 );
printf("String sent by the client is: %s\n", shared_area->string);
shmdt((void *) shared_area); // удаляем отображение
shmctl(shmid, IPC_RMID, 0); // удаляем блок разделяемой памяти
return EXIT_SUCCESS;
}

Программа-клиент:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <errno.h
>
#define MAXLEN 512
struct shmem_t {
int locked;
char string[MAXLEN];
};
int main() {
key_t key;
int shmid;
struct shmem_t * shared_area;
char * greeting = "Hello, shared memory world!";
key = ftok("/home/andrei/.bashrc", 1); // генерация ключа
/* получаем идентификатор блока разделяемой памяти */
shmid = shmget(key, sizeof(struct shmem_t), 0777);
/* отображаем блок разделяемой памяти в адресное пространства процесса */
shared_area = ( struct shmem_t * ) shmat( shmid, 0, 0);
strcpy(shared_area->string, gr
eeting);
shared_area->locked = 1;
shmdt((void *) shared_area); // удаляем отображение
printf("String is sent!\n");
return EXIT_SUCCESS;
}

Семафоры

Семафоры широко используются как средство синхронизации потоков, принадлежащих одному или разным процессам, а также для разделения доступа к критическим ресурсам.

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

Работу семафоров можно продемонстрировать на примере разделения доступа к некоему ресурсу, не допускающему одновременного использования несколькими потоками. Перед тем, как начать работу с таким ресурсом, поток должен проверить, не используется ли ресурс другим потоком. Если ресурс занят, поток ожидает его высвобождения. Если ресурс свободен, поток начинает работу с ресурсом, при этом поток сообщает системе, что ресурс занят и другие потоки не могут получить к нему доступ. По окончании работы с ресурсом поток сигнализирует системе о высвобождении ресурса.

SVID описывает функции, предназначенные для работы с семафорами. Семафоры создаются при помощи функции semget, объявленной в файле sem.h. У функции semget три параметра. В первом параметре функции передается уникальный ключ, аналогичный по смыслу соответствующему параметру функций msgget и shmget. Второй параметр указывает, сколько семафоров необходимо создать. Дело в том, что часто для синхронизации процессов требуется более одного семафора, и функция semget позволяет создавать несколько семафоров за один вызов.

Для управления состоянием семафоров служит функция semop. Первый параметр этой функции – идентификатор, возвращенный функцией semget. Второй параметр функции semop представляет собой указатель на массив структур, содержащих данные об операции над семафорами. В третьем параметре передается число записей в массиве, которое должно соответствовать числу семафоров. Структура, служащая для передачи в semop данных об операциях над семафорами, называется sembuf. И структура и функция объявляются в файле sys/sem.h. В структуре sembuf определено много полей, из которых явным образом обязательно должны быть заданы три:
short sem_num – номер семафора, над которым выполняется операция (нумерация начинается с нуля).
short sem_op – число, изменяющее состояние семафора.
short sem_flg – дополнительные флаги.

Состояние семафора определяется значением некоторой внутренней переменной. Это значение может быть изменено при помощи поля sem_op. Следующая функция иллюстрирует логику работы функции semop в зависимости от значения переменной состояния (semvalue) и переменной sem_op.

void semaphore (int semvalue, int sem_op) {
if (sem_op != 0) {
if (sem_op < 0) while (semvalue < ABS(sem_op));
semvalue += sem_op;
}
else while (semvalue != 0);
}

Отрицательное значение sem_op соответствует операции проверки доступности ресурса и вызывает приостановку потока, если ресурс недоступен. Положительное значение сигнализирует о высвобождении ресурса.

В поле sem_flg устанавливаются флаги для функции semop. Обязательным является флаг SEM_UNDO. Кроме этого можно указать флаг IPC_NOWAIT. В этом случае при наступлении “блокирующей ситуации” функция semop не приостанавливает выполнение программы, а возвращает значение –1.

Для управления семафорами используется функция semctl, аналогичная shmctl. У этой функции, однако, есть дополнительные возможности. Например, вызов

semctl(semid, semnum, SETVAL, value);

позволяет установить значение переменной, управляющей состоянием семафора, равным value. Рассмотрим простой пример:

#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#define KEY 1421
int main() {
int pid, semid;
struct sembuf buf;
semid = semget(KEY, 1, 0600|IPC_CREAT|IPC_EXCL);
buf.sem_num = 0;
buf.sem_flg = SEM_UNDO;
semctl(semid, 0, SETVAL, 0);
pid = fork();
if (pid == 0) {
sleep(1);
printf("Hello\n");
buf.sem_op = 1;
semop(semid, (struct sembuf*) &buf, 1);
buf.sem_op = -2;
semop(semid, (struct sembuf*) &buf, 1);
} else {
buf.sem_op = -1;
semop(semid, (struct sembuf*) &buf, 1);
printf("World!\n");
buf.sem_op = 2;
semop(semid, (struct sembuf*) &buf, 1);
semctl(semid, 0, IPC_RMID, 0);
}
return 0;
}

Сначала мы создаем два процесса. Родительский процесс печатает слово “World!” только после того, как дочерний процесс напечатает слово “Hello”. Вызов sleep(1) приводится для наглядности. Второй вызов функции semop с параметром sem_op равным 2 производится для того, чтобы дочерний процесс не завершился до окончания родительского (при работе с семафорами следует уделять внимание синхронизации завершения работы).

В заключение рассмотрим две консольные команды, служащие для управления IPC-объектами. Команда ipcs выводит данные о наличествующих в данный момент очередях сообщений, блоках разделяемой памяти и семафорах. Команда ipcrm позволяет удалять эти IPC-объекты. Данная команда может быть полезна в процессе отладки IPC-приложений при появлении “бесхозных” IPC-объектов.


Статья была опубликована в журнале "Программист", #11, 2001 г. Перепечатка статьи возможна только с разрешения редакции журнала.