Signals
Сигналы и каналы
На лекции будем разбирать способы взаимодействия процессов.
Сигналы
Имеется список стандартных сигналов. С помощью них ядро может взаимодействовать с процессом.
man 7 signal - посмотреть список сигналов
Стандартные действия (диспозиции):
- Term (преврать процесс)
- Ign (игнорировать)
- Core (преврать и записать информацию)
- Stop (остановить процесс)
- Cont (возобновить процесс)
На большиство сигналов можем повесить обработчик:
#include <signal.h>
#include <unistd.h>
void sayhi(int signo) {
write(STDOUT_FILENO, "hi guys\n", 8);
}
int main() {
char c;
signal(SIGINT, sayhi); // не самый современный и лучший механизм
while (1) {
pause(); // дожидаемся сигнала
// Причем работает даже так:
c++;
}
}
Обработчик выполняется на нашем стеке. Когда ядру надо выполнить обработку сигнала. Отступает от rsp 128 байт. Записывает старые значения регистров, адрес возврата и т.д. И запускает нашу функцию. (Механизм как в корутинах).
Если обращаться к одной переменной из процесса и обработчика:
#include <signal.h>
#include <unistd.h>
int counter;
void sayhi(int signo) {
if (++counter >= 3) {
_exit(0);
}
write(STDOUT_FILENO, "hi guys\n", 8);
}
int main() {
char c;
signal(SIGINT, sayhi);
while (1) {
++counter;
}
}
То из-за того, что у нас закодироваться операции могут как угодно и прийти сигнал может когда угодно, то может все поломаться.
Стадарт языка C предлагает следующий механизм:
volatile sig_atomic_t counter;
Хотим что-то печатать:
#include <signal.h>
#include <unistd.h>
int counter;
void sayhi(int signo) {
if (++counter >= 3) {
_exit(0);
}
printf("hello world");
write(STDOUT_FILENO, "hi guys\n", 8);
}
int main() {
char c;
signal(SIGINT, sayhi);
while (1) {
printf("hello world");
}
}
Опять же все сломается: printf - это блокирующая функция => если мы попробуем использовать ее вместе с прерываниями, то просто рискуем получить deadlock.
Из обработчика сигналов, можем использовать только специальные функции: системные вызовы, функции стандартной библиотеки без глобального состояния. (Фукции с пометкой async singal sace - man signal-safety)
Исторические проблемы:
На разных системах signal() может работать по-разному.
Поэтому лучше использовать sigaction()
void sayhi(int signo) {
if (++counter >= 3) {
_exit(0);
}
printf("hello world");
}
int main() {
char c;
struct sigaction act = {
.sa_handler = sayhi,
};
sigaction(SIGINT, &act, NULL);
while (1) {
++c;
}
}
При этом sigaction() намного более тонко настраивать обработчик. Особенно флаги.
По умолчанию, мы блокируем сигнал, пока его обрабатываем.
Флаг SA_RESETHAND позволяет эмулировать поведение старого Unix: если во время системного вызова приходит сигнал, то производится выход из вызова со специальной ошибкой и надо вручную перезапускать сискол. Сейчас это делается автоматически - флаг SA_RESTART.
Доставка сигналов
В ядре есть два ветктора, для отслеживания сигналов:
pending: bitvector 0000001000000000
blocked: bitvector 0000000000010000
Поэтому нельзя послать несколько одинаковых сигналов.
Существует системный вызов: sigprocmask() для управления блокирующей маской.
Заблокируем какой-то сигнал на время выполнения read():
while(1) {
sigset_t sigint, oldmask;
sigemptyset(&sigint);
sigaddset(&sigint, SIGINT);
sigprocmask(SIG_BLOCK, &sigint, &oldmask);
read(STDIN_FILENO, &c, 1);
sigprocmask(SIG_SETMASK, &oldmask, NULL);
pause();
}
sigsuspend(&oldmask) повзоляет либо ждать прихода сигнала, либо сразу его выполнить, если он пришел и был заблокирован.
До этого мы говорили про асинхронные сигналы.
Если мы отправляем сигнал себе в команду или получаем сигнал от ядра в ожидаемые моменты, то это синхронные сигналы.
Есть сигналы, которые нельзя блокировать, по типу SIGKILL
Таймеры и другие
Есть сигнал Alarm.
А еще есть механизм таймеров (setitimer)
Каналы
Системный вызов pipe() создает 2 файловых дескриптора, которые соединены неименованным каналом, один конец - пишущий, второй - читающий.
Когда процесс пытается вписать в закртый канал, от ядра приходит сигнал SIGPIPE.