Suicidal Tendencies

And other strange behaviors of Erlang exit signals

Brujo Benavides
Erlang Battleground
6 min readJun 26, 2017

--

As recently stated on erlang-questions:

Exit signals are funny things.

Suicidal Tendencies — I’m not a fan of the band, but I always liked their name.

Regular Exit Signals

In Erlang, when 2 processes (let’s call them π and ∂) are linked and one of them dies, an exit signal is propagated to the other. At that point a couple of things can happen:

  • If π died because it successfully finished the evaluation of its function, the exit signal will have reason normal
    · If ∂ was not trapping exits, nothing will happen there: ∂ will keep running as usual
    · If ∂ was trapping exits, it will receive a message {‘EXIT’, π, normal}
  • If π terminates abnormally (i.e. with en error, because another linked process terminated abnormally too, etc.), the exit signal will have a reason other than normal
    · If ∂ was not trapping exits, it will terminate itself with the same reason
    · If ∂ was trapping exits, it will receive a message {‘EXIT’, π, Reason}

exit/1

There is a way to terminate the process in which your code is running and choose the reason for the termination. That is exit/1. You can use exit(normal) to produce the same results as if the process has finished executing its code or exit(Reason) to terminate the process with any other reason you would like. For example:

1> process_flag(trap_exit, true).
false
2> spawn_link(fun() -> io:format("1~n"), exit(normal), io:format("2~n") end).
1
<0.70.0>
3> flush().
Shell got {'EXIT',<0.70.0>,normal}
ok
4> spawn_link(fun() -> io:format("1~n"), exit(abnormal), io:format("2~n") end).
1
<0.73.0>
5> flush().
Shell got {'EXIT',<0.73.0>,abnormal}
ok
6>

I started trapping exits first because that way what’s happening is clearer. You can see that 1 is always printed, 2 is never printed and the exit signals are propagated in the same way as I described in the previous section.

And this is where the intuitive and reasonable stuff ends…

exit/2

Erlang provides as well a function to, in theory, send an exit signal to a process without affecting the current process (as stated here): exit/2. This function has many corner cases and gotchas.

Intuitive Scenarios

Let’s start with the expected scenario: you use exit(Pid, Reason), Reason is not normal nor kill and either Pid is trapping exits or it’s not linked to the calling process:

1> process_flag(trap_exit, true).
false
2> Self = self().
<0.67.0>
3> Pid = spawn_link(fun() -> exit(Self, bye),
3> timer:sleep(60000)
3> end).
<0.71.0>
4> flush().
Shell got {'EXIT',<0.71.0>,bye}
ok
5> is_process_alive(Pid).
true
6>

As you can see, since Self (a.k.a. the console) is trapping exits, it receives an exit signal from the spawned process but the process remains very much alive.

1> Pid = spawn(fun() -> receive Shell -> exit(Shell, bye) end,
1> timer:sleep(60000)
1> end).
<0.69.0>
2> Pid ! self().
** exception exit: bye
3> is_process_alive(Pid).
true
4>

As you can see, I had to do a trick here since Exit Signals travel faster than evaluation results and I would not have been able to assign anything to Pid otherwise. In the end, same thing happened, the shell got the exit signal and died, but the other process remained alive.

The Boomerang Situation

Now let me show you a very very similar example…

1> Pid = spawn_link(fun() -> receive Shell -> exit(Shell, bye) end,
1> timer:sleep(60000)
1> end).
<0.69.0>
2> Pid ! self().
** exception exit: bye
3> is_process_alive(Pid).
false
4>

Woah! What happened there? Well… since Pid was linked to the shell and not trapping exits, and the shell died with reason byePid died with reason bye as well. As… ehm… expected.

The Unconditional Killer

You can also use the atom kill as the second argument on exit/2. According to the docs…

If Reason is the atom kill, that is, if exit(Pid, kill) is called, an untrappable exit signal is sent to Pid, which unconditionally exits with exit reason killed.

…and also…

An exception to [the rule about trapping exits] is if the exit reason is kill, that is if exit(Pid,kill) has been called. This unconditionally terminates the process, regardless of if it is trapping exit signals.

1> process_flag(trap_exit, true).
false
2> Self = self().
<0.67.0>
3> spawn(fun() -> exit(Self, kill) end).
** exception exit: killed
4>

That looks consistent with the above, but wait… because there is another way to generate an exit signal with reason kill

1> process_flag(trap_exit, true).
false
2> Self = self().
<0.67.0>
3> spawn_link(fun() -> exit(kill) end).
<0.71.0>
4> flush().
Shell got {'EXIT',<0.71.0>,kill}
ok
5>

A-ha! So… exit(Pid, kill) is not just sending an exit signal with reason kill. It is actually doing something else: It’s sending an untrappable exit signal. The reason is actually irrelevant, we will never have access to it anyway.

The “normal” Situation

So what happens when your Reason is normal? Well, if you don’t try to send the exit signal to yourself, you’re fine…

1> Self = self().
<0.67.0>
2> Pid = spawn_link(fun() -> exit(Self, normal),
2> timer:sleep(60000)
2> end).
<0.70.0>
3> process_flag(trap_exit, true).
false
4> Pid2 = spawn_link(fun() -> exit(Self, normal),
timer:sleep(60000)
end).
<0.73.0>
5> flush().
Shell got {'EXIT',<0.73.0>,normal}
ok
6> is_process_alive(Pid).
true
7> is_process_alive(Pid2).
true
8>

Nothing different than what’s described in the docs. But what if you send an exit signal to yourself?

Suicidal Tendencies

Finally, we arrive to the core of ’s email. What if, for some mysterious reason you decide to send an exit signal to yourself, as if you have terminated? Let’s first try to check the exit signals, by trapping them.

1> process_flag(trap_exit, true).
false
2> exit(self(), bye).
true
3> flush().
Shell got {'EXIT',<0.67.0>,bye}
ok
4> exit(self(), normal).
true
5> flush().
Shell got {'EXIT',<0.67.0>,normal}
ok
6> exit(self(), kill).
** exception exit: killed
7>

If you’re trapping exit signals, nothing out of the ordinary happens: unless the reason is kill, you just receive the corresponding messages and keep moving on. But if you are not trapping exits…

1> exit(self(), bye).
** exception exit: bye
2> exit(self(), normal).
** exception exit: normal
3>

What’s going on here?

As Ben Murphy pointed out:

It’s a very deliberate decision by someone.

erts/emulator/beam/bif.c:

erts_send_exit_signal(BIF_P,
BIF_P->common.id,
rp,
&rp_locks,
BIF_ARG_2,
NIL,
NULL,
BIF_P==rp ? ERTS_XSIG_FLG_NO_IGN_NORMAL : 0);

It sends ERTS_XSIG_FLG_NO_IGN_NORMAL if you are sending a signal to yourself.

The reason behind that may never be known, but I will give you my wildest guess: Let’s say you’re a happy living process in the Erlang VM and you suddenly receive a signal indicating that you are in fact dead. Do you want to keep living as a ghost? Do you want to defy the all-knowing power of the BEAM and stay alive even when in all likelihood you are expected to be dead? No, ma’am… If I just died, I’ll make sure not to be living.

Erlang & Elixir Factory Lite Buenos Aires 2017

For the last time, I would like to finish this article with a little self-promotion…

In less than a week, we’ll have the first South American Erlang & Elixir conference ever in Buenos Aires and I want to invite you all to it.

The programme is already online and the list of talks is impressive (BTW you can still submit a Lightning Talk!). The BEAM community is growing fast and this will be a great place to start connecting with everybody. So, all my south-american readers: come join us! It will be buenísimo!

--

--