Лаборатория Tarantool

Системное программирование

Лекция 4:

Сигналы. Аппаратные и программные прерывания, их природа. Top and bottom halves. Сигналы и системные вызовы, контекст сигнала.

The presentation is outdated and not maintained anymore. Please, refer to the English version for the most actual slides.

Новости

Дедлайны:

  • планировщик корутин: 19 марта, штраф -3
  • shell: 2 апреля

Процесс

.text

.data

.stack

.heap

.stack

.stack

Файловые дескрипторы

Очередь сигналов

IPC

Память

Файловые дескрипторы

struct task_struct {
        /* ... */
        struct files_struct *files;
        /* ... */
};

struct files_struct {
	struct fdtable *fdt;
};

struct fdtable {
	unsigned int max_fds;
	struct file **fd;
};

struct file {
	struct path	  f_path;
	struct inode	  *f_inode;
	atomic_long_t	  f_count;
	unsigned int 	  f_flags;
	fmode_t		  f_mode;
	loff_t		  f_pos;
	const struct cred *f_cred;
};

Процесс хранит открытые файлы

В таблице дескрипторов

Таблица - обычный массив

Дескриптор в ядре - это структура. В user space - число. Индекс в массиве дескрипторов.

Адресное пространство процесса

0x0

0xffffffff

.text

.data

.bss

.heap

.stack

.env

Уровни памяти

Скорость

Объем

  • Регистры
  • Кеш
  • Оперативная память
  • Флэш память
  • Магнитная память

Системные вызовы

int 0x80
sysenter/sysexit
syscall/sysret

Процесс

int 0x80

Адрес обработчика?

0x0...0x80...

IDT - Interrupt Descriptor Table

Процесс

syscall
sysenter

План лекции

  • Прерывания в процессоре
  • Прерывания в ядре
  • Сигналы

Прерывания [1]

Примеры процессорных прерываний?

  • От счетчика времени
  • Page fault:
    • Запись в Copy-On-Write страницу
    • Доступ к не своей памяти
    • Нет страницы в TLB
  • Отладка
  • Системные вызовы

1 балл

Прерывания [2]

Аппаратные

Программные

Для асинхронного обмена с I/O аппаратурой

Они же software, exceptions, traps

Для асинхронной обработки исключительных ситуаций

Аппаратные прерывания [1]

Прочитать диск?

syscall(read)

Файловая система

Драйвер

Шина

Как узнать, когда чтение готово?

Как это сделать для всех устройств?

Аппаратные прерывания [2]

PIC

CPU

Dev 1

Есть, что сказать процессору?

Programmable Interrupt Controller

00000

Dev 2

00001

00001

00001

00010

Генерируются аппаратурой для привлечения внимания процессора

Программные прерывания [1]

...
push   %rbp
mov    %rsp,%rbp
sub    $0x10,%rsp
lea    0x1c1(%rip),%rax
mov    %edi,-0x4(%rbp)
mov    %esi,-0x8(%rbp)
mov    -0x8(%rbp),%esi
mov    -0x4(%rbp),%edx
mov    %rax,%rdi
mov    $0x0,%al
callq  100001df2 <_recursive+0x82>
mov    %eax,-0xc(%rbp)
callq  100001440 <_coro_yield>
cmpl   $0x5,-0x4(%rbp)
...

Генерируются процессором для обработки сложных и неожиданных ситуаций

...
mov    %eax,-0x18(%rbp)
callq  100001660 <_coro_new>
mov    %rax,-0x20(%rbp)
callq  100001440 <_coro_yield>
mov    -0xc(%rbp),%eax
add    $0x20,%rsp
pop    %rbp
retq
...

Программные прерывания [2]

Ошибки (exceptions)

Ловушки (traps)

Обработчик

Если исправлена, то возврат на начало инструкции

  • Нет страницы в TLB
  • запись в COW память
  • поломки железа
  • деление на 0
  • неизвестная инструкция
  • адрес не выровнен
  • запрещенная память

Обработчик

...
mov    %edi,-0x4(%rbp)
mov    %esi,-0x8(%rbp)
mov    -0x8(%rbp),%esi
mov    -0x4(%rbp),%edx
mov    %rax,%rdi
mov    $0x0,%al
...
...
mov    %edi,-0x4(%rbp)
mov    %esi,-0x8(%rbp)
int    0x03
mov    -0x4(%rbp),%edx
mov    %rax,%rdi
mov    $0x0,%al
...

Возврат к следующей инструкции

  • Инструкция int
  • syscall/sysenter
  • "Lock nop"

Прерывания [1]

Обработка прерывания

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

1 балл

  1. Как найти функцию-обработчик?
  2. Как прервать текущую задачу?

Прерывания [2] Обработка

  1. Контекст = регистры, и сохраняется на спец. стек, выделенный ядром каждому процессору
     
  2. Поиск обработчика в IDT - Interrupt Descriptor Table
     
  3. IDT - в обычной памяти. Адрес начала IDT в регистре idtr.
     
  4. По адресу из IDT прыжок в начало .text секции обработчика
     
  5. Потом восстановление контекста

IDT

idtr

0

255

Контекст

Память

.text обработчика

Стек для сохранения контекста

Прерывания [3] IDT

Номер Назначение
0 Деление на 0
1 Пошаговый режим выполнения
2 NMI - Non-Maskable Interrupt
3 Breakpoint
6 Некорректная инструкция
8 Double page fault
14 Page fault
NA Triple page fault
​32 - 255 ​- Пользовательские прерывания -

Прерывания в ядре [1]

  • Прерывание - IRQ, Interrupt ReQuest
  • Обработчик - ISR, Interrupt Service Routine
  • У обработчиков отдельный контекст, стек
  • Делятся на Top Half и Bottom Half

Top Half

  • Осознание причины прерывания
  • Сохранение срочных данных
  • Планирование Bottom Half
  • Разблокировка следующих прерываний

Bottom Half

  • Распаковка и проверка данных
  • Их доставка ядру/пользователю
  • Пробуждение потоков, обновление планировщика

Прерывания в ядре [2] API

int
request_threaded_irq(unsigned int irq,
                     irq_handler_t handler,
                     irq_handler_t thread_fn,
                     unsigned long irqflags,
                     const char *devname,
                     void *dev_id);

Номер линии на PIC

Обработчик каждого прерывания

Флаги блокировки прерываний, флаги разделения линии для нескольких устройств

Произвольные данные. Обычно структура устройства

Прерывания в ядре [2] Время

static irqreturn_t rtc_interrupt(int irq, void *dev_id)
{
	spin_lock(&rtc_lock);
	rtc_irq_data += 0x100;
	rtc_irq_data &= ~0xff;
	if (is_hpet_enabled())
		rtc_irq_data |= (unsigned long)irq & 0xF0;
	else
		rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) & 0xF0);

	if (rtc_status & RTC_TIMER_ON)
		mod_timer(&rtc_irq_timer, jiffies + HZ/rtc_freq + 2*HZ/100);

	spin_unlock(&rtc_lock);
	wake_up_interruptible(&rtc_wait);
	kill_fasync(&rtc_async_queue, SIGIO, POLL_IN);
	return IRQ_HANDLED;
}

/* ... */

static int __init rtc_init(void)
{
        /* ... */
	if (request_irq(rtc_irq, rtc_interrupt, IRQF_SHARED, "rtc",
			(void *)&rtc_port)) {
		rtc_has_irq = 0;
		printk(KERN_ERR "rtc: cannot register IRQ %d\n", rtc_irq);
		return -EIO;
	}
        /* ... */
}

Сигнатура обработчика прерывания - номер линии и те же данные, что сохраняли в request_threaded_irq

Таймер тикает с частотой HZ - используется для обновления счетчика времени

Пробуждение планировщика

Регистрация обработчика - линия делится, имя "rtc", ...

Прерывания в ядре [3] /proc/interrupts

/proc/interrupts - зарегистрированные обработчики прерываний

           CPU0       CPU1       
  0:         30          0   IO-APIC   2-edge      timer
  1:       7418          0   IO-APIC   1-edge      i8042
  8:          0          0   IO-APIC   8-edge      rtc0
  9:          0          0   IO-APIC   9-fasteoi   acpi
 12:          0       2216   IO-APIC  12-edge      i8042
 14:     129007          0   IO-APIC  14-edge      ata_piix
 15:          0          0   IO-APIC  15-edge      ata_piix
 18:          0          0   IO-APIC  18-fasteoi   vboxvideo
 19:      92901        588   IO-APIC  19-fasteoi   enp0s3
 20:          0      69311   IO-APIC  20-fasteoi   vboxguest
 21:      13110      44644   IO-APIC  21-fasteoi   ahci[0000:00:0d.0], snd_intel8x0
 22:         29          0   IO-APIC  22-fasteoi   ohci_hcd:usb1
NMI:          0          0   Non-maskable interrupts
LOC:    2967493    4647820   Local timer interrupts
SPU:          0          0   Spurious interrupts
PMI:          0          0   Performance monitoring interrupts
IWI:          0          0   IRQ work interrupts
RTR:          0          0   APIC ICR read retries
RES:     340157     261945   Rescheduling interrupts
CAL:      22307      12237   Function call interrupts
TLB:       3463       4360   TLB shootdowns
TRM:          0          0   Thermal event interrupts
THR:          0          0   Threshold APIC interrupts
DFR:          0          0   Deferred Error APIC interrupts
MCE:          0          0   Machine check exceptions
MCP:        418        418   Machine check polls
HYP:          0          0   Hypervisor callback interrupts
           CPU0       CPU1       
  0:         30          0   IO-APIC   2-edge      timer
  1:       7430          0   IO-APIC   1-edge      i8042
  8:          0          0   IO-APIC   8-edge      rtc0
  9:          0          0   IO-APIC   9-fasteoi   acpi
 12:          0       2240   IO-APIC  12-edge      i8042
 14:     129069          0   IO-APIC  14-edge      ata_piix
 15:          0          0   IO-APIC  15-edge      ata_piix
 18:          0          0   IO-APIC  18-fasteoi   vboxvideo
 19:      92932        588   IO-APIC  19-fasteoi   enp0s3
 20:          0      69584   IO-APIC  20-fasteoi   vboxguest
 21:      13110      44695   IO-APIC  21-fasteoi   ahci[0000:00:0d.0], snd_intel8x0
 22:         29          0   IO-APIC  22-fasteoi   ohci_hcd:usb1
NMI:          0          0   Non-maskable interrupts
LOC:    2970432    4649435   Local timer interrupts
SPU:          0          0   Spurious interrupts
PMI:          0          0   Performance monitoring interrupts
IWI:          0          0   IRQ work interrupts
RTR:          0          0   APIC ICR read retries
RES:     342167     263540   Rescheduling interrupts
CAL:      22316      12237   Function call interrupts
TLB:       3463       4360   TLB shootdowns
TRM:          0          0   Thermal event interrupts
THR:          0          0   Threshold APIC interrupts
DFR:          0          0   Deferred Error APIC interrupts
MCE:          0          0   Machine check exceptions
MCP:        419        419   Machine check polls
HYP:          0          0   Hypervisor callback interrupts
           CPU0       CPU1       
  0:         30          0   IO-APIC   2-edge      timer
  1:       7442          0   IO-APIC   1-edge      i8042
  8:          0          0   IO-APIC   8-edge      rtc0
  9:          0          0   IO-APIC   9-fasteoi   acpi
 12:          0       2296   IO-APIC  12-edge      i8042
 14:     129125          0   IO-APIC  14-edge      ata_piix
 15:          0          0   IO-APIC  15-edge      ata_piix
 18:          0          0   IO-APIC  18-fasteoi   vboxvideo
 19:      92960        588   IO-APIC  19-fasteoi   enp0s3
 20:          0      70033   IO-APIC  20-fasteoi   vboxguest
 21:      13110      44733   IO-APIC  21-fasteoi   ahci[0000:00:0d.0], snd_intel8x0
 22:         29          0   IO-APIC  22-fasteoi   ohci_hcd:usb1
NMI:          0          0   Non-maskable interrupts
LOC:    2972773    4652064   Local timer interrupts
SPU:          0          0   Spurious interrupts
PMI:          0          0   Performance monitoring interrupts
IWI:          0          0   IRQ work interrupts
RTR:          0          0   APIC ICR read retries
RES:     346096     265997   Rescheduling interrupts
CAL:      22322      12258   Function call interrupts
TLB:       3463       4381   TLB shootdowns
TRM:          0          0   Thermal event interrupts
THR:          0          0   Threshold APIC interrupts
DFR:          0          0   Deferred Error APIC interrupts
MCE:          0          0   Machine check exceptions
MCP:        419        419   Machine check polls
HYP:          0          0   Hypervisor callback interrupts
           CPU0       CPU1       
  0:         30          0   IO-APIC   2-edge      timer
  1:       7451          0   IO-APIC   1-edge      i8042
  8:          0          0   IO-APIC   8-edge      rtc0
  9:          0          0   IO-APIC   9-fasteoi   acpi
 12:          0       2352   IO-APIC  12-edge      i8042
 14:     129171          0   IO-APIC  14-edge      ata_piix
 15:          0          0   IO-APIC  15-edge      ata_piix
 18:          0          0   IO-APIC  18-fasteoi   vboxvideo
 19:      92995        588   IO-APIC  19-fasteoi   enp0s3
 20:          0      70370   IO-APIC  20-fasteoi   vboxguest
 21:      13110      44769   IO-APIC  21-fasteoi   ahci[0000:00:0d.0], snd_intel8x0
 22:         29          0   IO-APIC  22-fasteoi   ohci_hcd:usb1
NMI:          0          0   Non-maskable interrupts
LOC:    2974684    4654345   Local timer interrupts
SPU:          0          0   Spurious interrupts
PMI:          0          0   Performance monitoring interrupts
IWI:          0          0   IRQ work interrupts
RTR:          0          0   APIC ICR read retries
RES:     349138     268939   Rescheduling interrupts
CAL:      22334      12260   Function call interrupts
TLB:       3463       4383   TLB shootdowns
TRM:          0          0   Thermal event interrupts
THR:          0          0   Threshold APIC interrupts
DFR:          0          0   Deferred Error APIC interrupts
MCE:          0          0   Machine check exceptions
MCP:        419        419   Machine check polls
HYP:          0          0   Hypervisor callback interrupts

Прерывания в ядре [4] Bottom Half

Решения для откладывания bottom half:

  • softirq
  • tasklet
  • workqueue
$ ps aux | grep softirq
root         7  0.0  0.0      S    мар20   0:00 [ksoftirqd/0]
root        16  0.0  0.0      S    мар20   0:00 [ksoftirqd/1]

Рабочие потоки ядра для выполнения softirq и tasklet-ов

Программные прерывания - основная часть ядра, нет Bottom Half

Аппаратные прерывания - драйвера, есть Bottom Half

Вопросы [1]

Два типа прерываний, и для чего нужны?

Аппаратные и программные. Аппаратные для асинхронной работы с периферией. Программные для обработки исключительных ситуаций.

1 балла

Вопросы [2]

Куда поступают аппаратные прерывания?

PIC или APIC - Programmable Interrupt Controller. Только оттуда они подаются на процессор, по одному.

1 балла

Вопросы [3]

Откуда и как ядро берет обработчик прерывания?

Из IDT - Interrupt Descriptor Table. Она лежит в памяти, и ее начало сохранено ядром в регистр idtr.

1 балл

Сигналы [1]

Прерывания - только в ядре.

Сигналы - аналог прерываний в userspace. Сигнал сохраняет контекст процесса и вызывает обработчик.

Сигнал Назначение Действие по умолчанию
SIGABRT Аварийное завершение Завершить процесс
SIGALRM Таймауты Завершить процесс
SIGINT Остановка пользователем Завершить процесс
SIGSEGV Ошибка памяти Завершить процесс
SIGUSR1, SIGUSR2 Пользовательские сигналы Завершить процесс
SIGCHLD Завершился процесс потомок Игнорирование
SIGKILL Принудительное завершение Завершить процесс
... - Еще десятки сигналов - ...

Сигналы [2] signal

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

"Старый, но не бесполезный"

Номер сигнала

Функция-обработчик. Принимает номер сигнала

Сигналы [3] signal

static void
on_new_signal(int signum)
{
	switch(signum) {
	case SIGINT:
		printf("caught sigint\n");
		break;
	case SIGUSR1:
		printf("caught usr1\n");
		break;
	case SIGUSR2:
		printf("caught usr2\n");
		break;
	default:
		printf("caught unknown signal\n");
		exit(-1);
	}
}

int
main(void)
{
	printf("my pid: %d\n", (int) getpid());
	signal(SIGINT, on_new_signal);
	signal(SIGUSR1, on_new_signal);
	signal(SIGUSR2, on_new_signal);
	while(true) pause();
	return 0;
}
$> gcc 1_basic_signal.c
$> ./a.out
my pid: 42645
$> kill -s SIGUSR1 42645
^C caught sigint
caught usr1
$> kill -s SIGUSR2 42645
caught usr2
$> kill 42645
Terminated
$>

Сигналы [4] signal

signal - устаревший вызов

  • Раньше срабатывание сигнала сбрасывало обработчик
  • Поведение не определено при > 1 потоков
  • Нельзя блокировать сигналы (≠ игнорировать)

Сигналы [5] signal

static bool is_sig_received = false;

static void
on_new_signal(int signum)
{
	printf("Received a signal\n");
	is_sig_received = true;
}

static void
interrupt(void)
{
	getchar();
}

int
main(void)
{
        printf("my pid: %d\n", (int) getpid());
	signal(SIGUSR1, on_new_signal);
	printf("Installed a handler\n");
	interrupt();
	while (! is_sig_received) {
		interrupt();
		printf("Wait for a signal\n");
		pause();
	}
	printf("Finish\n");
	return 0;
}
$> gcc 2_pause_hangs.c
$>./a.out
my pid: 42718
Installed a handler
$> kill -s SIGUSR1 42718
$> kill -s SIGUSR1 42769
$> kill 42769
Received a signal
                        <press Enter>
Finish
$>
$> ./a.out
my pid: 42769
Installed a handler
                        <press Enter>
Received a signal
                        <press Enter>
Wait for a signal
                        <press Enter>
                        <press Enter>
Terminated
$>

Сигналы [6]

Общие проблемы сигналов:

  • Сигнал может прервать системный вызов
  • Сигнал может прервать функцию, работающую с глобальными переменными
ssize_t rc;
while ((rc = read(fd, buf, size) == -1 && errno == EINTR);

Обычно "медленный" системный вызов надо повторять, если ошибка EINTR - вызов был прерван сигналом

Сигналы [7]

static int alarm_cnt = 0;

static void
do_malloc(void)
{
	for (int i = 0; i < 10; ++i)
		free(malloc(1024 * 1024 * i));
}

static void
on_new_signal(int signum)
{
	printf("Interrupted %d times\n", ++alarm_cnt);
	do_malloc();
}

int
main(void)
{
	printf("my pid: %d\n", (int) getpid());
	signal(SIGUSR1, on_new_signal);
	while (true)
		do_malloc();
	return 0;
}
$> gcc 3_intr_malloc.c
$>./a.out
my pid: 42888
$> while kill -s SIGUSR1 42888; do sleep 0; done
$>
Interrupted 1 times
Interrupted 2 times
Illegal instruction: 4
$>

Сигналы [8]

int kill(pid_t pid, int sig);

int raise(int sig);

int pause(void);

unsigned int alarm(unsigned int seconds);

Послать сигнал процессу

PID получателя

Номер сигнала. Не обязательно SIGKILL

Послать сигнал себе

Остановить поток, пока не будет получен и обработан любой сигнал

Отложенно послать себе SIGALRM

Через сколько секунд

Сигналы [9] sigprocmask

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

Управление маской сигналов потока. Если сигнал есть в маске - он блокируется.

Что сделать с маской в аргументе?

  • SIG_BLOCK - добавить ее к маске потока
  • SIG_UBLOCK - вычесть из маски потока
  • SIG_SETMASK - установить как новую маску

Маска для выполнения действия 'how'

Выходной параметр для получения старой маски

int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);

Заполнение и проверка маски

int sigpending(sigset_t *set);

Проверка, какие сигналы пришли и ожидают разблокировки

Сигналы [10] sigprocmask

static int cnt = 0;

static void on_new_signal(int signum)
{
	printf("Processed a signal %dth time\n",
               ++cnt);
}

int main(void)
{
	printf("Start\n");
	signal(SIGINT, on_new_signal);
	sigset_t oldm, newm, pendingm;
	sigemptyset(&newm);
	sigaddset(&newm, SIGINT);
	printf("Block SIGINT\n");
	sigprocmask(SIG_BLOCK, &newm, &oldm);
	getchar();

	sigpending(&pendingm);
	if (sigismember(&pendingm, SIGINT))
		printf("SIGINT is pending\n");

	printf("Unblock SIGINT\n");
	sigprocmask(SIG_SETMASK, &oldm, NULL);
	while(cnt < 4)
		pause();
	return 0;
}
$> gcc 4_sigprocmask.c
$>./a.out
Start
Block SIGINT
^C ^C ^C ^C
                             <press Enter>
SIGINT is pending
Unblock SIGINT
Processed a signal 1th time
^C Processed a signal 2th time
^C Processed a signal 3th time
^C Processed a signal 4th time
$>

Sigaction [1]

int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

struct sigaction {
        void (*sa_handler)(int);
        void (*sa_sigaction)(int, siginfo_t *, void *);
        sigset_t sa_mask;
        int sa_flags;
};

Номер сигнала

Настраиваемая установка обработчика сигнала

Новый обработчик

Получение старого обработчика

Два варианта обработки сигнала

Очень подробная информация о причине сигнала

ucontext_t * - сохраненный контекст прерванного потока

Маска сигналов для добавление к маске потока на время обработки сигнала. Этот сигнал включен неявно по умолчанию

Управление поведением обработчика:

  • SA_NODEFER - не блокировать данный сигнал на время обработки
  • SA_ONSTACK - выбрать обработчику другой стек
  • SA_RESETHAND - сброс обработчика после первого выполнения
  • SA_SIGINFO - использовать обработчик sa_sigaction
    ...

Sigaction [2]

static void
on_new_signal(int signum, siginfo_t *info,
              void *context)
{
	printf("Segfault: signum = %d, si_signo = "\
               "%d, si_addr = %p, si_code = \n",
	       signum, info->si_signo, info->si_addr);
	switch (info->si_code) {
	case SEGV_MAPERR:
		printf("not mapped\n");
		break;
	case SEGV_ACCERR:
		printf("no permission\n");
		break;
	default:
		printf("%d\n", info->si_code);
		break;
	}
	exit(-1);
}

int
main(void)
{
	struct sigaction act;
	act.sa_sigaction = on_new_signal;
	sigemptyset(&act.sa_mask);
	act.sa_flags = SA_SIGINFO;
	sigaction(SIGSEGV, &act, NULL);
	char *ptr = (char *) 10;
	*ptr = 100;
	return 0;
}
$> gcc 5_sigaction_info.c
$>./a.out
Segfault: signum = 11, si_signo = 11, si_addr = 0xa, si_code = not mapped
$>

Для использования расширенного обработчика - SA_SIGINFO

Указатель, очевидно, указывает на невалидную память

Это = SIGSEGV, обработчик которого печатает место и причину

Sigaction [3.1]

static int finished = 0;
static int id = 0;

static int is_usr1_blocked(void)
{
	sigset_t old;
	sigprocmask(0, NULL, &old);
	return sigismember(&old, SIGUSR1);
}

static void on_new_signal(int signum)
{
	int my_id = ++id;
	printf("Begin processing %d, "\
               "usr1 block = %d\n", my_id,
	       is_usr1_blocked());
	if (my_id == 1)
		raise(signum);
	printf("End processing %d\n", my_id);
	++finished;
}

Проверка, что сигнал SIGUSR1 заблокирован

Обработчик сигнала вызывает свой собственный сигнал еще раз

int main(int argc, char **argv)
{
	struct sigaction act;
	act.sa_handler = on_new_signal;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	for (int i = 1; i < argc; ++i) {
		if (strcmp(argv[1], "nodefer") == 0)
			act.sa_flags |= SA_NODEFER;
	}
	sigaction(SIGUSR1, &act, NULL);
	printf("Before raises, usr1 block = %d\n",
               is_usr1_blocked());
	raise(SIGUSR1);
	while (finished != 2)
		sched_yield();
	printf("After raises, usr1 block = %d\n",
               is_usr1_blocked());
	return 0;
}

Если в main передан аргумент nodefer, то сигнал ставится с флагом SA_NODEFER - не блокировать сигнал в обработчике

Sigaction [3.2]

$> gcc 6_sigaction_mask.c
$>./a.out
Before raises, usr1 block = 0
Begin processing 1, usr1 block = 1
End processing 1
Begin processing 2, usr1 block = 1
End processing 2
After raises, usr1 block = 0
$>
$>
$> ./a.out nodefer
Before raises, usr1 block = 0
Begin processing 1, usr1 block = 0
Begin processing 2, usr1 block = 0
End processing 2
End processing 1
After raises, usr1 block = 0
$>

Sigaction [4.1]

static int finished = 0;
static jmp_buf buf;

static int is_usr1_blocked(void)
{
	sigset_t old;
	sigprocmask(0, NULL, &old);
	return sigismember(&old, SIGUSR1);
}

static void on_new_signal(int signum)
{
	printf("Process signal, usr1 block = %d\n",
               is_usr1_blocked());
	++finished;
	longjmp(buf, 1);
}

Из обработчика выполняется прыжок вместо return

int main(void)
{
	struct sigaction act;
	act.sa_handler = on_new_signal;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	sigaction(SIGUSR1, &act, NULL);
	printf("Before raise, usr1 block = %d\n",
               is_usr1_blocked());
	if (setjmp(buf) == 0)
		raise(SIGUSR1);
	while (finished != 1)
		sched_yield();
	printf("After raise, usr1 block = %d\n",
               is_usr1_blocked());
	return 0;
}

Когда прыжок вернется сюда, setjmp выдаст 1. На первый вызов будет 0, и тогда один раз вызывается сигнал

Sigaction [4.2]

$> gcc 7_sigaction_jmp.c
$>./a.out
Before raise, usr1 block = 0
Process signal, usr1 block = 1
After raise, usr1 block = 1​
$>
$> gcc 7_sigaction_jmp.c
$>./a.out
Before raise, usr1 block = 0
Process signal, usr1 block = 1
After raise, usr1 block = 0
$>

Linux

Mac

Sigaction [5.1]

static int finished = 0;
static sigjmp_buf buf;

static int is_usr1_blocked(void)
{
	sigset_t old;
	sigprocmask(0, NULL, &old);
	return sigismember(&old, SIGUSR1);
}

static void on_new_signal(int signum)
{
	printf("Process signal, usr1 block = %d\n",
               is_usr1_blocked());
	++finished;
	siglongjmp(buf, 1);
}

Из обработчика выполняется прыжок вместо return

int
main(void)
{
	struct sigaction act;
	act.sa_handler = on_new_signal;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	sigaction(SIGUSR1, &act, NULL);
	printf("Before raise, usr1 block = %d\n",
               is_usr1_blocked());
	if (sigsetjmp(buf, 1) == 0)
		raise(SIGUSR1);
	while (finished != 1)
		sched_yield();
	printf("After raise, usr1 block = %d\n",
               is_usr1_blocked());
	return 0;
}

Когда прыжок вернется сюда, sigsetjmp выдаст 1 и откатит маску на момент вызова sigsetjmp.

Sigaction [5.2]

$> gcc 8_sigaction_sigjmp.c
$>./a.out
Before raise, usr1 block = 0
Process signal, usr1 block = 1
After raise, usr1 block = 0
$>
$> gcc 8_sigaction_sigjmp.c
$>./a.out
Before raise, usr1 block = 0
Process signal, usr1 block = 1
After raise, usr1 block = 0
$>

Linux

Mac

Sigsuspend [1]

int sigsuspend(const sigset_t *mask);

Атомарная подмена маски потока, ожидание сигнала, возврат маски

Маска для временного применения

Sigsuspend [2.1]

static int finished = 0;

static int is_int_blocked(void)
{
	sigset_t old;
	sigprocmask(0, NULL, &old);
	return sigismember(&old, SIGINT);
}

static void on_new_signal(int signum)
{
	printf("Process signal, int block = %d\n",
               is_int_blocked());
	++finished;
}
int main(void)
{
	printf("Before main, int block = %d\n",
               is_int_blocked());
	sigset_t block, old;
	sigemptyset(&block);
	sigaddset(&block, SIGINT);
	sigprocmask(SIG_BLOCK, &block, &old);

	struct sigaction act;
	act.sa_handler = on_new_signal;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	sigaction(SIGINT, &act, NULL);
	sigemptyset(&block);
	while (finished != 1)
		sigsuspend(&block);
	sigprocmask(SIG_SETMASK, &old, NULL);
	printf("After main, int block = %d\n",
               is_int_blocked());
	return 0;
}

Ранее SIGINT заблокирован, но каждая итерация sigsuspend разблокирует его в попытке получить нужный сигнал

Sigsuspend [2.2]

$> gcc 9_sigsuspend.c
$>./a.out
Before main, int block = 0
^C Process signal, int block = 1
After main, int block = 0
$>

Sigaltstack [1]

int sigaltstack(const stack_t *ss, stack_t *old_ss);

typedef struct {
        void *ss_sp;
        int ss_flags;
        size_t ss_size;
} stack_t;

Задание альтернативного стека для обработчиков сигналов

Новый стек

Выходной параметр, старый стек

Память стека и ее размер.

  • SIGSTKSZ - рекомендуемый размер
  • MINSIGSTKSZ - минимально возможный размер

Флаги управления стеком:

  • SS_DISABLE - отключить альтернативный стек, использовать поточный

Sigaltstack [2.1]

static int finished = 0;

#define handle_error() ({printf("error = %s\n", \
                         strerror(errno)); exit(-1); })

static void
on_new_signal(int signum)
{
	volatile int local_var;
	printf("Process signal, stack = %p\n", &local_var);
	++finished;
}

static void
stack_create(int size)
{
	int page_size = getpagesize();
	size = size + (page_size - size % page_size);
	if (size < SIGSTKSZ)
		size = SIGSTKSZ;
	stack_t s;
	s.ss_sp = malloc(size);
	printf("Page size = %d, new stack begin = %p, "\
               "stack end = %p\n", page_size, s.ss_sp,
               s.ss_sp + size);
	s.ss_flags = 0;
	s.ss_size = size;
	if (sigaltstack(&s, NULL) != 0)
		handle_error();
}
static void
stack_destroy(void)
{
	stack_t s;
	if (sigaltstack(NULL, &s) != 0)
		handle_error();
	s.ss_flags = SS_DISABLE;
	if (sigaltstack(&s, NULL) != 0)
		handle_error();
	printf("Destroy stack %p\n", s.ss_sp);
	free(s.ss_sp);
}

Размер стека нужен кратным размеру страницы и не меньше рекомендуемого

Откуда память взять - не важно. Хоть с кучи, хоть с этого же стека

Теперь любой сигнал, sigaction которого позвали с SA_ONSTACK, будет обрабатываться на этом стеке

Выключение стека не простое. Передать NULL первым аргументом не поможет. Нужно достать старый стек

Установить флаг SS_DISABLE - это отвяжет стек от ядра, сигналов, и можно делать с ним, что угодно

Sigaltstack [2.2]

static void
wait_signal(void)
{
	int need = finished + 1;
	raise(SIGUSR1);
	while (finished < need)
		sched_yield();
}

int
main(int argc, char **argv)
{
	struct sigaction act;
	printf("Main, stack = %p\n", &act);
	act.sa_handler = on_new_signal;
	sigemptyset(&act.sa_mask);

	act.sa_flags = SA_ONSTACK;
	stack_create(1024 * 64);
	if (sigaction(SIGUSR1, &act, NULL) != 0)
		handle_error();
	wait_signal();
	stack_destroy();

	act.sa_flags = 0;
	if (sigaction(SIGUSR1, &act, NULL) != 0)
		handle_error();
	wait_signal();
	return 0;
}

Пробуем вызвать на альтернативном стеке

И на обычном. Адреса должны сильно отличаться

$> gcc 10_sigaltstacks.c
$>./a.out
Main, stack = 0x7ffeea01aa30

Page size = 4096, new stack begin = 0x105bfd000, stack end = 0x105c1d000

Process signal, stack = 0x105c1ca98

Destroy stack 0x105bfd000

Process signal, stack = 0x7ffeea01a438
$>

После установки нового стека видно, что теперь обработчик его использует

После удаления стека снова стал использоваться поточный

Libcoro [1]

На sigaction, sigaltstack, sigsuspend и siglongjmp можно написать библиотеку корутин

Но! Есть проблема - результат setjmp портится после return, согласно стандарту. Почему так? Не помешает ли это библиотеке?

Libcoro [2]

static volatile jmp_buf buf;
static volatile bool stop = false;
static volatile int total = 0;

static void
testf2(void)
{
	setjmp(buf);
	if (++total > 10) {
		printf("Bad exit\n");
		exit(-1);
	}
}

static void
testf(void)
{
	testf2();
}

int
main(void)
{
	testf();
	if (! stop) {
		stop = true;
		printf("Аfter first call\n");
		longjmp(buf, 1);
	}
	return 0;
}
$> gcc 11_setjmp_problem.c
$>./a.out
After first call
Segmentation fault
$>

Почему?

Return использует адрес возврата, положенный заранее на стек. После return это место стека может быть перетерто другими вызовами. Прыгнуть обратно можно, но return не сделать.

2 балла

Libcoro [3]

static volatile jmp_buf buf;
static volatile bool stop = false;
static volatile int total = 0;

static void
testf2(void)
{
	setjmp(buf);
	if (++total > 10) {
		printf("Bad exit\n");
		exit(-1);
	}
}

static void
testf(void)
{
	testf2();
}

int
main(void)
{
	testf();
	if (! stop) {
		stop = true;
		printf("Аfter first call\n");
		longjmp(buf, 1);
	}
	return 0;
}

Стек

-

<ret addr to main>

<ret addr to testf>

-

-

-

-

Ошибка! На стеке нет адреса возврата!

Libcoro [4]

Возможное решение - никогда не делать return. Все адреса возврата сохранять самому в стек jmp_buf объектов. Это делает coro_jmp.h библиотека из первого ДЗ.

Минусы:

  • Дополнительная память на много jmp_buf
  • В С++ не будут зваться деструкторы
  • Невозможно использовать локальные переменные
  • Легко ошибиться

Libcoro [5]

Создание одной корутины:

  • Создаем стек, ставим его для обработки сигналов
  • Пускаем сигнал, ядро забросит нас на этот стек
  • Запоминаем это место в  setjmp, возвращаемся из сигнала
  • Открепляем стек от обработчика

Сделать return оттуда будет нельзя, так как на этом стеке адреса возврата нет после его открепления. Значит оттуда в конце надо "спрыгивать". Куда? - нужно сделать шедулер. Пользователь будет через него "ловить спрыгивающие" корутины.

Libcoro [6]

Libcoro [7.1]

#define coro_count 3
static struct coro *coros[coro_count];

static void
recursive(int deep, int id)
{
	printf("%d: recursive step %d\n", id, deep);
	coro_yield();
	if (deep < 2)
		recursive(deep + 1, id);
	printf("%d: finish recursive step %d\n", id, deep);
}

static int
coro_func(void *ptr)
{
	int id = (int) ptr;
	printf("%d: coro is started\n", id);
	coro_yield();
	recursive(0, id);
	return id;
}

static int
coro_tree_func(void *ptr)
{
	int id = (int) ptr;
	printf("%d: coro line is started\n", id);
	coro_yield();
	if (id > 1) {
		printf("%d: coro line end\n", id);
	} else {
		printf("%d: coro line next\n", id);
		coro_new(coro_tree_func,
			 (void *) (id + 1));
	}
	coro_yield();
	return id;
}

int
main(void)
{
	printf("Start main\n");
	coro_sched_init();
	for (int i = 0; i < coro_count; ++i) {
		coros[i] = coro_new(coro_func,
				    (void *) i);
	}
	struct coro *c;
	while ((c = coro_sched_wait()) != NULL) {
		printf("Finished %d\n", coro_status(c));
		coro_delete(c);
	}
	printf("Finished simple\n");

	coro_new(coro_tree_func, (void *) 0);
	while ((c = coro_sched_wait()) != NULL) {
		printf("Finished %d\n", coro_status(c));
		coro_delete(c);
	}
	printf("Finish main\n");
	return 0;
}

Libcoro [7.2]

$> gcc 12_libcoro_example.c 12_libcoro.c
$>./a.out
Start main
2: coro is started
1: coro is started
0: coro is started
2: recursive step 0
1: recursive step 0
0: recursive step 0
2: recursive step 1
1: recursive step 1
0: recursive step 1
2: recursive step 2
1: recursive step 2
0: recursive step 2
2: finish recursive step 2
2: finish recursive step 1
2: finish recursive step 0
Finished 2
1: finish recursive step 2
1: finish recursive step 1
1: finish recursive step 0
Finished 1
0: finish recursive step 2
0: finish recursive step 1
0: finish recursive step 0
Finished 0
Finished simple

0: coro line is started
0: coro line next
1: coro line is started
Finished 0
1: coro line next
2: coro line is started
Finished 1
2: coro line end
Finished 2
Finish main

Размышления [1]

Нет. Почему:

  • Корутины на swapcontext можно сделать и без сигналов
  • На Linux до версии 4.7 прыжки по этому ucontext приводили к падению процесса

Расширенный обработчик в sigaction принимает ucontext_t прерванного потока. Можно ли использовать это для создания корутин?

Размышления [2]

Можно ли сделать на сигналах вытесняющую многозадачность?

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

Для потоков - да, но тоже не стоит. Планировщик ядра уже это и так делает.

Заключение

Обратная связь: goo.gl/forms/TAeraYrqJcil7GDt1

и на портале Техносферы

В следующий раз:

Файловая система. Виртуальная ФС в ядре. Файлы, их типы. I/O операции и их планировщики в ядре. Page cache. Режимы работы с файлом.

Системное программирование 4

By Vladislav Shpilevoy

Системное программирование 4

Прерывания. Аппаратные и программные, их природа, назначение. Обработка прерываний. Сигналы - зачем нужни они, как устроены. Обработка сигналов, особенности, контекст выполнения, longjump, top and bottom halves. /proc/interrupts. signal, sigaction.

  • 2,327