In the following code, I have not made the variable quit has volatile sig_atomic_t. I have left it as a plain int.
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#define UNUSED(x) (void) (x)
int quit;
void sigusr1_handler(int sig)
{
UNUSED(sig);
write(1, "handler\n", 8);
quit = 1;
}
int main()
{
struct sigaction sa;
sa.sa_handler = sigusr1_handler;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
quit = 0;
while (!quit) {
printf("Working ...\n");
sleep(1);
}
printf("Exiting ...\n");
return 0;
}
Since the quit variable is not specified as volatile, I was expecting that the compiler's optimizer would optimize the while-loop in the code to:
while (1) {
printf("Working ...\n");
sleep(1);
}
But I don't see this happening. On one terminal, I run the following.
$ gcc -O3 foo.c && ./a.out
Working ...
Working ...
On another terminal, I send SIGUSR1 to my program.
$ pkill -USR1 a.out
On the first terminal, the output shows that the program's signal handler is invoked and the while-loop quits.
Working ...
Working ...
handler
Exiting ...
$
How can I demonstrate the optimization of the loop due to quit not being volatile?
It can be difficult to force the compiler to optimize the load of quit out from the while conditional. I was able to do it by removing the body of the while loop:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#define UNUSED(x) (void) (x)
int quit;
void sigusr1_handler(int sig)
{
UNUSED(sig);
write(1, "handler\n", 8);
quit = 1;
}
int main()
{
struct sigaction sa;
sa.sa_handler = sigusr1_handler;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
quit = 0;
while (!quit) {
//printf("Working ...\n");
//sleep(1);
}
printf("Exiting ...\n");
return 0;
}
When compiling for x86-64 with -O3 on gcc version 5.4 this results in an infinite loop which doesn't check the quit variable. Notice the jump-to-self at .L5:
.LC0:
.string "handler\n"
sigusr1_handler(int):
sub rsp, 8
mov edx, 8
mov esi, OFFSET FLAT:.LC0
mov edi, 1
call write
mov DWORD PTR quit[rip], 1
add rsp, 8
ret
.LC2:
.string "sigaction"
main:
sub rsp, 168
lea rdi, [rsp+8]
mov QWORD PTR [rsp], OFFSET FLAT:sigusr1_handler(int)
mov DWORD PTR [rsp+136], 0
call sigemptyset
xor edx, edx
mov rsi, rsp
mov edi, 10
call sigaction
cmp eax, -1
je .L7
mov DWORD PTR quit[rip], 0
.L5:
jmp .L5
.L7:
mov edi, OFFSET FLAT:.LC2
call perror
mov eax, 1
add rsp, 168
ret
quit:
.zero 4
Changing the definition to volatile int quit; causes the correct behavior. See the mov, test, and je instructions at .L6:
.LC0:
.string "handler\n"
sigusr1_handler(int):
sub rsp, 8
mov edx, 8
mov esi, OFFSET FLAT:.LC0
mov edi, 1
call write
mov DWORD PTR quit[rip], 1
add rsp, 8
ret
.LC2:
.string "sigaction"
.LC3:
.string "Exiting ..."
main:
sub rsp, 168
lea rdi, [rsp+8]
mov QWORD PTR [rsp], OFFSET FLAT:sigusr1_handler(int)
mov DWORD PTR [rsp+136], 0
call sigemptyset
xor edx, edx
mov rsi, rsp
mov edi, 10
call sigaction
cmp eax, -1
je .L11
mov DWORD PTR quit[rip], 0
.L6:
mov eax, DWORD PTR quit[rip]
test eax, eax
je .L6
mov edi, OFFSET FLAT:.LC3
call puts
xor eax, eax
.L5:
add rsp, 168
ret
.L11:
mov edi, OFFSET FLAT:.LC2
call perror
mov eax, 1
jmp .L5
quit:
.zero 4
You can play with both examples here: without volatile and with volatile.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With