Программирование в стандарте POSIX

       

Сигналы


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

Согласно стандарту POSIX-2001, под сигналом понимается механизм, с помощью которого процесс или поток управления уведомляют о некотором событии, произошедшем в системе, или подвергают воздействию этого события. Примерами подобных событий могут служить аппаратные исключительные ситуации и специфические действия процессов. Термин "сигнал" используется также для обозначения самого события.

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

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

В каждом процессе определены действия, предпринимаемые в ответ на все предусмотренные системой сигналы. Говорят, что сигнал доставлен процессу, когда взято для выполнения действие, соответствующее данным процессу и сигналу. сигнал принят процессом, когда он выбран и возвращен одной из функций sigwait().

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

У каждого потока управления есть маска сигналов, определяющая набор блокируемых сигналов. Обычно она достается в наследство от родительского потока.

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

SIG_DFL

Подразумеваемые действия, зависящие от сигнала. Они описаны в заголовочном файле <signal.h>.

SIG_IGN

Игнорировать сигнал. Доставка сигнала не оказывает воздействия на процесс.

указатель на функцию

Обработать сигнал, выполнив при его доставке заданную функцию. После завершения функции обработки процесс возобновляет выполнение с точки прерывания. Обычно функция обработки вызывается в соответствии со следующим C-заголовком: void func (int signo); где signo - номер доставленного сигнала.

Первоначально, до входа в функцию main(), реакция на все сигналы установлена как SIG_DFL или SIG_IGN.

Функция называется асинхронно-сигнально-безопасной (АСБ), если ее можно вызывать без каких-либо ограничений при обработке сигналов. В стандарте POSIX-2001 имеется список функций, которые должны быть либо повторно входимыми, либо непрерываемыми сигналами, что превращает их в АСБ-функции. В этот список включены 117 функций, в том числе почти все из рассматриваемых нами.

Если сигнал доставляется потоку, а реакция заключается в завершении, остановке или продолжении, весь процесс должен завершиться, остановиться или продолжиться.

Перейдем к изложению возможностей по генерации сигналов. Выше была кратко рассмотрена служебная программа kill как средство терминирования процессов извне. На самом деле она посылает заданный сигнал; то же делает и одноименная функция (см. листинг 8.6).

#include <signal.h> int kill (pid_t pid, int sig);

Листинг 8.6. Описание функции kill(). (html, txt)

Сигнал задается аргументом sig, значение которого может быть нулевым; в этом случае действия функции kill() сводятся к проверке допустимости значения pid (нулевой результат - признак успешного завершения kill()).



Если pid > 0, это значение трактуется как идентификатор процесса. При нулевом значении pid   сигнал посылается всем процессам из той же группы, что и вызывающий. Если значение pid равно -1, адресатами являются все процессы, которым вызывающий имеет право посылать сигналы. При прочих отрицательных значениях pid   сигнал посылается группе процессов, чей идентификатор равен абсолютной величине pid.

Процесс имеет право послать сигнал адресату, заданному аргументом pid, если он (процесс) имеет соответствующие привилегии или его реальный или действующий идентификатор пользователя совпадает с реальным или сохраненным ПДП-идентификатором адресата.

У служебной программы kill имеется полезная опция -l, позволяющая увидеть соответствие между номерами сигналов и их мнемоническими именами. Результат выполнения команды kill -l может выглядеть так, как показано в листинге 8.7.

Листинг 8.7. Возможный результат выполнения команды kill -l. (html, txt)





SIGABRT

Сигнал аварийного завершения процесса. Подразумеваемая реакция предусматривает, помимо аварийного завершения, создание файла с образом памяти процесса.

SIGALRM

Срабатывание будильника. Подразумеваемая реакция - аварийное завершение процесса.

SIGBUS

Ошибка системной шины как следствие обращения к неопределенной области памяти. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGCHLD

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

SIGCONT

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

SIGFPE

Некорректная арифметическая операция. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGHUP

Сигнал разъединения. Подразумеваемая реакция - аварийное завершение процесса.

SIGILL

Некорректная команда. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGINT

Сигнал прерывания, поступивший с терминала. Подразумеваемая реакция - аварийное завершение процесса.

SIGKILL

Уничтожение процесса (этот сигнал нельзя перехватить для обработки или проигнорировать). Подразумеваемая реакция - аварийное завершение процесса.

SIGPIPE

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

SIGQUIT

Сигнал выхода, поступивший с терминала. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGSEGV

Некорректное обращение к памяти. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGSTOP

Остановка выполнения (этот сигнал нельзя перехватить для обработки или проигнорировать). Подразумеваемая реакция - остановка процесса.

SIGTERM

Сигнал терминирования. Подразумеваемая реакция - аварийное завершение процесса.

SIGTSTP

Сигнал остановки, поступивший с терминала. Подразумеваемая реакция - остановка процесса.



SIGTTIN

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

SIGTTOU

Попытка записи из фонового процесса. Подразумеваемая реакция - остановка процесса.

SIGUSR1, SIGUSR2

Определяемые пользователем сигналы. Подразумеваемая реакция - аварийное завершение процесса.

SIGPOLL

Опрашиваемое событие. Подразумеваемая реакция - аварийное завершение процесса.

SIGPROF

Срабатывание таймера профилирования. Подразумеваемая реакция - аварийное завершение процесса.

SIGSYS

Некорректный системный вызов. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGTRAP

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

SIGURG

Высокоскоростное поступление данных в сокет. Подразумеваемая реакция - игнорирование.

SIGVTALRM

Срабатывание виртуального таймера. Подразумеваемая реакция - аварийное завершение процесса.

SIGXCPU

Исчерпан лимит процессорного времени. Подразумеваемая реакция - аварийное завершение и создание файла с образом памяти процесса.

SIGXFSZ

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

Процесс (поток управления) может послать сигнал самому себе с помощью функции raise() (см. листинг 8.8). Для процесса вызов raise() эквивалентен kill (getpid(), sig);

#include <signal.h> int raise (int sig);

Листинг 8.8. Описание функции raise().

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

#include <stdlib.h> void abort (void);

Листинг 8.9. Описание функции abort().

Опросить и изменить способ обработки сигналов позволяет функция sigaction() (см. листинг 8.10).

#include <signal.h> int sigaction (int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);



Листинг 8.10. Описание функции sigaction().

Для описания способа обработки сигнала используется структура sigaction, которая должна содержать по крайней мере следующие поля:

void (*sa_handler) (int); /* Указатель на функцию обработки сигнала */ /* или один из макросов SIG_DFL или SIG_IGN */ sigset_t sa_mask; /* Дополнительный набор сигналов, блокируемых */ /* на время выполнения функции обработки */ int sa_flags; /* Флаги, влияющие на поведение сигнала */ void (*sa_sigaction) (int, siginfo_t *, void *); /* Указатель на функцию обработки сигнала */

Приложение, соответствующее стандарту, не должно одновременно использовать поля обработчиков sa_handler и sa_sigaction.

Тип sigset_t может быть целочисленным или структурным и представлять набор сигналов (см. далее).

Тип siginfo_t должен быть структурным по крайней мере со следующими полями:

int si_signo; /* Номер сигнала */ int si_errno; /* Значение переменной errno, ассоциированное с данным сигналом */ int si_code; /* Код, идентифицирующий причину сигнала */ pid_t si_pid; /* Идентификатор процесса, пославшего сигнал */ uid_t si_uid; /* Реальный идентификатор пользователя процесса, пославшего сигнал */ void *si_addr; /* Адрес, вызвавший генерацию сигнала */ int si_status; /* Статус завершения порожденного процесса */ long si_band; /* Событие, связанное с сигналом SIGPOLL */

В заголовочном файле <signal.h> определены именованные константы, предназначенные для работы с полем si_code, значения которого могут быть как специфичными для конкретного сигнала, так и универсальными. К числу универсальных кодов относятся:

SI_USER

Сигнал послан функцией kill().

SI_QUEUE

Сигнал послан функцией sigqueue().

SI_TIMER

Сигнал сгенерирован в результате срабатывания таймера, установленного функцией timer_settime().

SI_ASYNCIO

Сигнал вызван завершением асинхронной операции ввода/вывода.

SI_MESGQ

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

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



ILL_ILLOPC

Некорректный код операции.

ILL_COPROC

Ошибка сопроцессора.

FPE_INTDIV

Целочисленное деление на нуль.

FPE_FLTOVF

Переполнение при выполнении операции вещественной арифметики.

FPE_FLTSUB

Индекс вне диапазона.

SEGV_MAPERR

Адрес не отображен на объект.

BUS_ADRALN

Некорректное выравнивание адреса.

BUS_ADRERR

Несуществующий физический адрес.

TRAP_BRKPT

Процесс достиг точки прерывания.

TRAP_TRACE

Срабатывание трассировки процесса.

CLD_EXITED

Завершение порожденного процесса.

CLD_STOPPED

Остановка порожденного процесса.

POLL_PRI

Поступили высокоприоритетные данные.

Вернемся непосредственно к описанию функции sigaction(). Если аргумент act отличен от NULL, он указывает на структуру, специфицирующую действия, которые будут ассоциированы с сигналом   sig. По адресу oact (если он не NULL) возвращаются сведения о прежних действиях. Если значение act есть NULL, обработка сигнала остается неизменной; подобный вызов можно использовать для опроса способа обработки сигналов.

Следующие флаги в поле sa_flags влияют на поведение сигнала   sig.

SA_NOCLDSTOP

Не генерировать сигнал   SIGCHLD при остановке или продолжении порожденного процесса (значение аргумента sig должно равняться SIGCHLD).

SA_RESETHAND

При входе в функцию обработки сигнала   sig установить подразумеваемую реакцию SIG_DFL и очистить флаг SA_SIGINFO (см. далее).

SA_SIGINFO

Если этот флаг не установлен и определена функция обработки сигнала   sig, она вызывается с одним целочисленным аргументом - номером сигнала. Соответственно, в приложении следует использовать поле sa_handler структуры sigaction. При установленном флаге SA_SIGINFO функция обработки вызывается с двумя дополнительными аргументами, как void func (int sig, siginfo_t *info, void *context); второй аргумент указывает на данные, поясняющие причину генерации сигнала, а третий может быть преобразован к указателю на тип ucontext_t - контекст процесса, прерванного доставкой сигнала. В этом случае приложение должно использовать поле sa_sigaction и поля структуры типа siginfo_t.


В частности, если значение si_code неположительно, сигнал был сгенерирован процессом с идентификатором si_pid и реальным идентификатором пользователя si_uid.

SA_NODEFER

По умолчанию обрабатываемый сигнал добавляется к маске сигналов процесса при входе в функцию обработки; флаг SA_NODEFER предписывает не делать этого, если только sig не фигурирует явным образом в sa_mask.

Опросить и изменить способ обработки сигналов можно и на уровне командного интерпретатора, посредством специальной встроенной команды trap:

trap [действие условие ...]

Аргумент "условие" может задаваться как EXIT (завершение командного интерпретатора) или как имя доставленного сигнала (без префикса SIG). При задании аргумента "действие" минус обозначает подразумеваемую реакцию, пустая цепочка ("") - игнорирование. Если в качестве действия задана команда, то при наступлении условия она обрабатывается как eval действие.

Команда trap без аргументов выдает на стандартный вывод список команд, ассоциированных с каждым из условий. Выдача имеет формат, пригодный для восстановления способа обработки сигналов (см. листинг 8.11).

save_traps=$(trap) . . . eval "$save_traps"

Листинг 8.11. Пример сохранения и восстановления способа обработки сигналов посредством специальной встроенной команды trap.

Обеспечить выполнение утилиты logout из домашнего каталога пользователя во время завершения командного интерпретатора можно с помощью команды, показанной в листинге 8.12.

trap '$HOME/logout' EXIT

Листинг 8.12. Пример использования специальной встроенной команды trap.

При перенаправлении вывода в файл приходится считаться с возможностью возникновения ошибок, специфичных для каналов. Чтобы защитить от них процедуры начальной загрузки, в ОС Lunix применяются связки из игнорирования и последующего восстановления подразумеваемой реакции на сигнал   SIGPIPE (см. листинг 8.13).

trap "" PIPE echo "$INITLOG_ARGS -n $0 -s \"$1\" -e 1" >&21 trap - PIPE



Листинг 8.13. Пример использования специальной встроенной команды trap для защиты от ошибок, специфичных для каналов.

К техническим аспектам можно отнести работу с наборами сигналов, которая выполняется посредством функций, показанных в листинге 8.14. Функции sigemptyset() и sigfillset() инициализируют набор, делая его, соответственно, пустым или "полным". Функция sigaddset() добавляет сигнал   signo к набору set, sigdelset() удаляет сигнал, а sigismember() проверяет вхождение в набор. Обычно признаком завершения является нулевой результат, в случае ошибки возвращается -1. Только sigismember() выдает 1, если сигнал   signo входит в набор set.

#include <signal.h> int sigemptyset (sigset_t *set); int sigfillset (sigset_t *set); int sigaddset (sigset_t *set, int signo); int sigdelset (sigset_t *set, int signo); int sigismember (const sigset_t *set, int signo);

Листинг 8.14. Описание функций для работы с наборами сигналов.

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

#include <signal.h> int sigprocmask (int how, const sigset_t *restrict set, sigset_t *restrict oset);

Листинг 8.15. Описание функции sigprocmask().

Если аргумент set отличен от NULL, он указывает на набор, используемый для изменения текущей маски сигналов. Аргумент how определяет способ изменения; он может принимать одно из трех значений: SIG_BLOCK (результирующая маска получается при объединении текущей и заданной аргументом set), SIG_SETMASK (результирующая маска устанавливается равной set) и SIG_UNBLOCK (маска set вычитается из текущей).

По адресу oset (если он не NULL) возвращается прежняя маска. Если значение set есть NULL, набор блокируемых сигналов остается неизменным; подобный вызов можно использовать для опроса текущей маски сигналов процесса.

Если к моменту завершения sigprocmask() будут существовать ждущие неблокированные сигналы, по крайней мере один из них должен быть доставлен до возврата из sigprocmask().



Нельзя блокировать сигналы, не допускающие игнорирования.

Функция sigpending() (см. листинг 8.16) позволяет выяснить набор блокированных сигналов, ожидающих доставки вызывающему процессу (потоку управления). Дождаться появления подобного сигнала можно с помощью функции sigwait() (см. листинг 8.17).

#include <signal.h> int sigpending (sigset_t *set);

Листинг 8.16. Описание функции sigpending().

#include <signal.h> int sigwait (const sigset_t *restrict set, int *restrict sig);

Листинг 8.17. Описание функции sigwait().

Функция sigwait() выбирает ждущий сигнал из заданного набора (он должен включать только блокированные сигналы), удаляет его из системного набора ждущих сигналов и помещает его номер по адресу, заданному аргументом sig. Если в момент вызова sigwait() нужного сигнала нет, процесс (поток управления) приостанавливается до появления такового.

Отметим, что стандарт POSIX-2001 не специфицирует воздействие функции sigwait() на обработку сигналов, включенных в набор set. Чтобы дождаться доставки обрабатываемого или терминирующего процесс сигнала, можно воспользоваться функцией pause() (см. листинг 8.18).

#include <unistd.h> int pause (void);

Листинг 8.18. Описание функции pause().

Функция pause() может ждать доставки сигнала неопределенно долго. Возврат из pause() осуществляется после возврата из функции обработки сигнала (результат при этом равен -1). Если прием сигнала вызывает завершение процесса, возврата из функции pause(), естественно, не происходит.

Несмотря на внешнюю простоту, использование функции pause() сопряжено с рядом тонкостей. При наивном подходе сначала проверяют некоторое условие, связанное с сигналом, и, если оно не выполнено (сигнал отсутствует), вызывают pause(). К сожалению, сигнал может быть доставлен в промежутке между проверкой и вызовом pause(), что нарушает логику работы процесса и способно привести к его зависанию. Решить подобную проблему позволяет функция sigsuspend() (см. листинг 8.19) в сочетании с рассмотренной выше функцией sigprocmask().



#include <signal.h> int sigsuspend (const sigset_t *sigmask);

Листинг 8.19. Описание функции sigsuspend().

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

Обычно парой функций sigprocmask() и sigsuspend() обрамляют критические интервалы. Перед входом в критический интервал посредством sigprocmask() блокируют некоторые сигналы, а на выходе вызывают sigsuspend() с маской, которую возвратила sigprocmask(), восстанавливая тем самым набор блокированных сигналов и дожидаясь их доставки.

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

#include <unistd.h> #include <signal.h> #include <stdio.h>

void abort (void) { struct sigaction sact; sigset_t sset;

/* Вытолкнем буфера */ (void) fflush (NULL);

/* Снимем блокировку сигнала SIGABRT */ if ((sigemptyset (&sset) == 0) && (sigaddset (&sset, SIGABRT) == 0)) { (void) sigprocmask (SIG_UNBLOCK, &sset, (sigset_t *) NULL); }

/* Пошлем себе сигнал SIGABRT. */ /* Возможно, его перехватит функция обработки, */ /* и тогда вызывающий процесс может не завершиться */ raise (SIGABRT);

/* Установим подразумеваемую реакцию на сигнал SIGABRT */ sact.sa_handler = SIG_DFL; sigfillset (&sact.sa_mask); sact.sa_flags = 0; (void) sigaction (SIGABRT, &sact, NULL);

/* Снова пошлем себе сигнал SIGABRT */ raise (SIGABRT);

/* Если сигнал снова не помог, попробуем еще одно средство завершения */ _exit (127); }

int main (void) { printf ("Перед вызовом abort()\n"); abort (); printf ("После вызова abort()\n"); return 0; }

Листинг 8.20. Упрощенная реализация функции abort() как пример использования функций работы с сигналами.

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



Еще одним примером использования механизма сигналов может служить приведенная в листинге 8.21 упрощенная реализация функции sleep(), предназначенной для "засыпания" на заданное число секунд. (Можно надеяться, что не описанные пока средства работы с временем интуитивно понятны.)

#include <unistd.h> #include <stdio.h> #include <signal.h> #include <time.h>

/* Функция обработки сигнала SIGALRM. */ /* Она ничего не делает, но игнорировать сигнал нельзя */ static void signal_handler (int sig) { /* В демонстрационных целях распечатаем номер обрабатываемого сигнала */ printf ("Принят сигнал %d\n", sig); }

/* Функция для "засыпания" на заданное число секунд */ /* Результат равен разности между заказанной и фактической */ /* продолжительностью "сна" */ unsigned int sleep (unsigned int seconds) { time_t before, after; unsigned int slept; sigset_t set, oset; struct sigaction act, oact;

if (seconds == 0) { return 0; }

/* Установим будильник на заданное время, */ /* но перед этим блокируем сигнал SIGALRM */ /* и зададим свою функцию обработки для него */ if ((sigemptyset (&set) < 0) || (sigaddset (&set, SIGALRM) < 0) || sigprocmask (SIG_BLOCK, &set, &oset)) { return seconds; }

act.sa_handler = signal_handler; act.sa_flags = 0; act.sa_mask = oset; if (sigaction (SIGALRM, &act, &oact) < 0) { return seconds; }

before = time ((time_t *) NULL); (void) alarm (seconds);

/* Как атомарное действие восстановим старую маску сигналов */ /* (в надежде, что она не блокирует SIGALRM) */ /* и станем ждать доставки обрабатываемого сигнала */ (void) sigsuspend (&oset); /* сигнал доставлен и обработан */

after = time ((time_t *) NULL);

/* Восстановим прежний способ обработки сигнала SIGALRM */ (void) sigaction (SIGALRM, &oact, (struct sigaction *) NULL);

/* Восстановим первоначальную маску сигналов */ (void) sigprocmask (SIG_SETMASK, &oset, (sigset_t *) NULL);

return ((slept = after - before) > seconds ? 0 : (seconds - slept)); }



int main (void) { struct sigaction act;

/* В демонстрационных целях установим обработку прерывания с клавиатуры */ act.sa_handler = signal_handler; (void) sigemptyset (&act.sa_mask); act.sa_flags = 0; (void) sigaction (SIGINT, &act, (struct sigaction *) NULL);

printf ("Заснем на 10 секунд\n"); printf ("Проснулись, не доспав %d секунд\n", sleep (10)); return (0); }

Листинг 8.21. Упрощенная реализация функции sleep() как пример использования механизма сигналов.

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

Вызвать "недосыпание" приведенной программы можно, послав ей сигнал   SIGALRM (например, посредством команды kill -s SIGALRM идентификатор_процесса) или SIGINT (путем нажатия на клавиатуре терминала комбинации клавиш CTRL+C).


Содержание раздела