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