Vladislav Shpilevoy PRO
Database C developer at Tarantool. Backend C++ developer at VirtualMinds.
Lecture 4:
Signals. Hardware and software interrupts, their nature. Top and bottom halves. Signals and system calls, signal context.
Version: 3
System programming
Examples of processor interrupts?
1 point
Hardware
Software
For asynchronous I/O with hardware
The same as exceptions, traps
For asynchronous handling of exceptions
Read from a disk?
syscall(read)
File system
Driver
Bus
How to find when the read is finished?
How to do it for each device?
PIC
CPU
Dev 1
Have something to tell the CPU?
Programmable Interrupt Controller
00000
Dev 2
00001
00001
00001
00010
Are generated by hardware to attract CPU's attention
...
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)
...
Are generated by CPU to process complex unexpected situations
...
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
...
Errors (exceptions)
Traps
Handler
If fixed, then return back before the instruction to redo it
Handler
... 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 ...
Return after the instruction, it is not redone
Interrupt handling
Save the context to the main memory. Find the function in a handler table.
1 point
IDT
idtr
0
255
Context
Memory
Handler start addr
Stack to save the context on
Index | Purpose |
---|---|
0 | 0 division |
1 | Stepwise execution |
2 | NMI - Non-Maskable Interrupt |
3 | Breakpoint |
6 | Invalid instruction |
8 | Double page fault |
14 | Page fault |
NA | Triple page fault |
32 - 255 | - User-defined interrupts - |
Top Half
Bottom Half
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);
Line number on PIC
Interrupt handler
Various internal flags
Arbitrary data used by the handler. Usually something related to the device.
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;
}
/* ... */
}
Interrupt handler signature - line number, and what was saved in request_threaded_irq
Timer ticks with HZ frequency - is used to update time counter
Scheduler wakeup
Handler registration - flag that the line is shared, name "rtc", ...
/proc/interrupts - registered interrupt handlers
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
Solutions to schedule bottom half:
$ ps aux | grep softirq
root 7 0.0 0.0 S mar20 0:00 [ksoftirqd/0]
root 16 0.0 0.0 S mar20 0:00 [ksoftirqd/1]
Worker threads in the kernel to process softirq and tasklets
Software interrupts - mostly processed in a kernel, no Bottom Half
Hardware interrupts - drivers, have Bottom Half
Two types of interrupts, when are used?
Hardware and software. Hardware for asynchronous work with periphery. Software to handle exceptions.
1 point
Where are hardware interrupts stored before processing?
PIC or APIC - Programmable Interrupt Controller. Only from there they are forwarded to CPU, one by one.
1 point
Where and how the kernel finds an interrupt handler?
From IDT - Interrupt Descriptor Table. It is stored in the main memory, its beginning is saved in idtr register.
1 point
Interrupts - kernel only.
Signals - an analogue of interrupts in userspace. Signal saves context of the process and calls a handler.
Signal | Purpose | Action by default |
---|---|---|
SIGABRT | Abort | Terminate process |
SIGALRM | Timeout | Terminate process |
SIGINT | Stopped by a user | Terminate process |
SIGSEGV | Memory error | Terminate process |
SIGUSR1, SIGUSR2 | User signals | Terminate process |
SIGCHLD | A child process is terminated | Ignore |
SIGKILL | Forced termination | Terminate process |
... | - Tens of other signals - | ... |
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
"Old, but not obsolete"
Signal number
Handler function. Takes a signal number
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 $>
signal() - deprecated
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 $>
Common problems of signals:
ssize_t rc;
while ((rc = read(fd, buf, size) == -1 && errno == EINTR);
Usually "slow" system call should be checked if the error is EINTR - the call has not failed, but was interrupted. Might retry.
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
$>
int kill(pid_t pid, int sig);
int raise(int sig);
int pause(void);
unsigned int alarm(unsigned int seconds);
Send a signal to a process
Receiver's PID
Signal number. Not necessary to be SIGKILL
Send a signal to self
Stop the thread until a signal is received
Send SIGALRM to self with a delay
After this number of seconds
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
Thread signal mask management. If a signal is present in the mask, it is blocked.
What to do with the mask in the argument?
Mask to use in 'how'-argument's action
Out-parameter to get the old mask
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);
Filling and checking of the mask
int sigpending(sigset_t *set);
Check what signals have arrived and are pending
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 $>
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;
};
Signal number
Set a signal handler for the whole process
New handler
Out-parameter to get the old handler
Two ways to process a signal
Very detailed information about the signal origins
ucontext_t * - saved context of an interrupted thread
Temporary mask to add to the thread mask during the signal processing. The signum signal is added implicitly by default
Handler behaviour management:
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 $>
To use an extended handler - SA_SIGINFO
The pointer, obviously, points at an invalid address
This is SIGSEGV handler which prints the exact place and the reason of the error
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;
}
Check that SIGUSR1 is blocked
Signal handler sends its own signal again
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;
}
If an argument nodefer is passed to main then the signal handler is called with SA_NODEFER - don't block the signal in handler
$> 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 $>
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);
}
Jump from the handler instead of 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;
}
When a jump is done to here, setjmp returns 1. On the first (real) call it is 0, and this is when the signal is sent. So basically 'setjmp' "returns" multiple times
$> 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
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);
}
Jump from the handler instead of 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;
}
When a jump is done to here, sigsetjmp returns 1 and reverts signal mask to the moment when sigsetjmp was called and returned first time.
$> 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
int sigsuspend(const sigset_t *mask);
Atomic change of the signal mask and wait for any signal
A temporary mask to wait for a signal with
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 was blocked, but each sigsuspend temporary unblocks it in an attempt to receive the needed signal
$> gcc 9_sigsuspend.c
$>./a.out
Before main, int block = 0
^C Process signal, int block = 1
After main, int block = 0
$>
int sigaltstack(const stack_t *ss, stack_t *old_ss);
typedef struct {
void *ss_sp;
int ss_flags;
size_t ss_size;
} stack_t;
An alternative stack for the signal handler
New stack
Out parameter, old stack
Stack memory and its size.
Stack management flags:
static int finished = 0;
#define handle_error() ({printf("error = %s\n", \
strerror(errno)); exit(-1); })
static void
on_new_signal(int signum)
{
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);
}
Stack size should be multiple of page size, and not less than recommended
Where to get the memory from? - not important. From heap, from the current stack, from mmap, from anywhere
Now any sinal with the established sigaction with SA_ONSTACK flag is going to be processed on that stack
Stack turn off is not simple. NULL as a first argument won't help. The old stack should be extracted more explicitly
Set flag SS_DISABLE - it unbinds the old stack from the kernel internal structures, and now it can be freed
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;
}
Try to handle on the alternative stack
And on a regular. The addresses should be very different
$>
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
$>
After the new stack is set, it is clear that the handler uses it
When the stack is dropped, the regular one is used again
With sigaction, sigaltstack, sigsuspend и siglongjmp a coroutine library can be created
But! There is a problem - first return after setjmp invalidates its result, jmp_buf. Why? Doesn't it make impossible to use it in our planned library?
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("After first call\n");
longjmp(buf, 1);
}
return 0;
}
$> gcc 11_setjmp_problem.c
$>./a.out
After first call
Segmentation fault
$>
Why?
<Answer is revealed on the lecture>
2 points
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("After first call\n");
longjmp(buf, 1);
}
return 0;
}
Stack
-
<ret addr to main>
<ret addr to testf>
-
-
-
-
Error! There is no return address on the stack!
A possible solution - never do returns. All return addresses are stored manually in a stack of jmp_buf objects
Problems:
Coroutine creation:
Now you can return to that stack back, using longjmp. But can't do return from it, because there is no return address already. It means, that you can only do jumps. To where? - this is where a scheduler appears. User will catch finished coroutines in that scheduler.
#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;
}
$> 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
Is it possible to implement a full featured preemptive scheduler in user-space using the signals hack?
For coroutines - yes, but it is not worth doing. There is no legal and clear way to bypass the problem of interruption in the middle of a critical or a globally visible section. All such functions need to block signals for the work time, what makes 'preemption' useless. Because literally any malloc/new in such a case would require signals blocked.
For threads - yes, but also not worth doing. But by another problem - it is already implemented in the kernel. The problem of global and critical sections doesn't exist here, because usually standard library functions are thread safe, and work ok even the current thread is paused.
Interrupts are of 2 types: software for exceptions on CPU and hardware for handling events from devices (disks, RAM, time source, etc).
Signals are similar to interrupts, but for processes. Raised by the kernel on certain events. Most of the signals can be intercepted and processed.
sigaction() is new and good, signal() is old and bad. Both allow to control signal handling per process.
Most of the signals can be blocked or ignored by threads of your choice via sigprocmask().
You can use your own stack for signal handling and do funny stuff with it like coroutines.
Signal handler is a very dangerous place where it is not safe to do almost anything. The list of safe-to-use standard functions is here: man7.org/linux/man-pages/man7/signal-safety.7.html
Lectures: slides.com/gerold103/decks/sysprog_eng
Next time:
Press on the heart, if like the lecture
File system. Virtual FS in the kernel. Files, their types. I/O operations and their kernel schedulers. Page cache. Modes of work with file.
By Vladislav Shpilevoy
Interrupts. Hardware and software, their nature, purpose. Interrupt handling. Purpose of signals, how they work. Signals handling, execution context, longjmp, top and bottom halves. /proc/interrupts. signal, sigaction.
Database C developer at Tarantool. Backend C++ developer at VirtualMinds.