What happends when sending SIGKILL to a Zombie Process in Linux?
To answer that question, you have to understand how signals are sent to a process and how a process exists in the kernel.
Each process is represented as a task_struct
inside the kernel (the definition is in the sched.h
header file and begins here). That struct holds information about the process; for instance the pid. The important information is in line 1566 where the associated signal is stored. This is set only if a signal is sent to the process.
A dead process or a zombie process still has a task_struct
. The struct remains, until the parent process (natural or by adoption) has called wait()
after receiving SIGCHLD
to reap its child process. When a signal is sent, the signal_struct
is set. It doesn't matter if the signal is a catchable one or not, in this case.
Signals are evaluated every time when the process runs. Or to be exact, before the process would run. The process is then in the TASK_RUNNING
state. The kernel runs the schedule()
routine which determines the next running process according to its scheduling algorithm. Assuming this process is the next running process, the value of the signal_struct
is evaluated, whether there is a waiting signal to be handled or not. If a signal handler is manually defined (via signal()
or sigaction()
), the registered function is executed, if not the signal's default action is executed. The default action depends on the signal being sent.
For instance, the SIGSTOP
signal's default handler will change the current process's state to TASK_STOPPED
and then run schedule()
to select a new process to run. Notice, SIGSTOP
is not catchable (like SIGKILL
), therefore there is no possibility to register a manual signal handler. In case of an uncatchable signal, the default action will always be executed.
To your question:
A defunct or dead process will never be determined by the scheduler to be in the TASK_RUNNING
state again. Thus the kernel will never run the signal handler (default or defined) for the corresponding signal, whichever signal is was. Therefore the exit_signal
will never be set again. The signal is "delivered" to the process by setting the signal_struct
in task_struct
of the process, but nothing else will happen, because the process will never run again. There is no code to run, all that remains of the process is that process struct.
However, if the parent process reaps its children by wait()
, the exit code it receives is the one when the process "initially" died. It doesn't matter if there is a signal waiting to be handled.
A zombie process is basically already dead. The only thing is that nobody has acknowledged its death yet so it continues occupying an entry in the process table as well as a control block (the structure the Linux kernel maintains for every thread in activity). Other resources like mandatory locks on files, shared memory segments, semaphores, etc. are reclaimed.
You cannot signal them because nobody can act upon this signal. Even fatal signals like KILL are useless since the process has already terminated its execution. You can try yourself:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid = fork();
if (pid == -1)
exit(-1);
if (pid > 0) {
//parent
printf("[parent]: I'm the parent, the pid of my child is %i\n"
"I'll start waiting for it in 10 seconds.\n", pid);
sleep(10);
int status;
wait(&status);
if (WIFSIGNALED(status)) {
printf("[parent]: My child has died from a signal: %i\n", WTERMSIG(status));
} else if (WIFEXITED(status)) {
printf("[parent]: My child has died from natural death\n");
} else {
printf("[parent]: I don't know what happened to my child\n");
}
} else {
//child
printf("[child]: I'm dying soon, try to kill me.\n");
sleep(5);
printf("[child]: Dying now!\n");
}
return 0;
}
Here, I start a process that forks and sleeps before waiting for its child. The child does nothing but sleep a little. You can kill the child when it's sleeping or just after it exits to see the difference:
$ make zombie
cc zombie.c -o zombie
$ ./zombie
[parent]: I'm the parent, the pid of my child is 16693
I'll start waiting for it in 10 seconds.
[child]: I'm dying soon, try to kill me.
# Here, I did "kill -15 16693" in another console
[parent]: My child has died from a signal: 15
$ ./zombie
[parent]: I'm the parent, the pid of my child is 16717
I'll start waiting for it in 10 seconds.
[child]: I'm dying soon, try to kill me.
[child]: Dying now!
# Here, I did "kill -15 16717" in another console
[parent]: My child has died from natural death