Process Control, Part 1: Wait macros, using signals - tcloaa/SystemProgramming GitHub Wiki
You can find the lowest 8 bits of the child's exit value (the return value of main()
or value included in exit()
):
Use the "Wait macros" - typically WIFEXITED
and WEXITSTATUS
. See wait
/waitpid
man page for more information).
int status;
pid_t child = fork();
if (child == -1)
return 1; //Failed
if (child > 0) { /* I am parent - wait for child*/
pid_t pid = waitpid(child, &status, 0); //on success, returns the process ID of the child whose state has changed;
if (pid != -1 && WIFEXITED(status)) {
int low8bits = WEXITSTATUS(status);
printf("Child Process %d returned %d" , pid, low8bits);
}
}
else { /* I am the child */
// do something interesting
execl("/bin/ls", "/bin/ls", ".", (char *) NULL); // "ls ."
}
For more details, man waitpid
.
-
WIFEXITED(status)
-
WEXITSTATUS(status)
-
WIFSIGNALED(status)
returns true if the child process was terminated by a signal. -
WTERMSIG(status)
returns the number of the signal that caused the child process to terminate. This macro should be employed only if WIFSIGNALED returned true.
-
SIGSTOP
: temporarily pause a running process If it succeeds it will freeze a process (the process will not be allocated anymore CPU time). -
SIGCONT
: To allow a process to resume execution
For example, Here's program that slowly prints a dot every second, up to 59 dots.
#include <unistd.h>
#include <stdio.h>
int main() {
printf("My pid is %d\n", getpid() );
int i = 60;
while(--i) {
write(1, ".",1);
sleep(1);
}
write(1, "Done!",5);
return 0;
}
We will first start the process in the background
(notice &
at the end).
Then send it a signal from the shell process by using the kill command.
localadmin@fa16-cs241-280:~/test$ ./a.out &
[1] 6620
localadmin@fa16-cs241-280:~/test$ My pid is 6620
........................
>kill -SIGSTOP 403
>kill -SIGCONT 403
In C, send a signal to the child using kill
POSIX call,
kill(child, SIGUSR1); // Send a user-defined signal to child
/*
SIGSTOP is the pause signal.
The only behavior is to pause the process;
the signal cannot be caught or ignored.
The shell uses pausing (and its counterpart, resuming via SIGCONT) to implement job control.
*/
// Stop the child process (the child cannot prevent this)
kill(child, SIGSTOP);
/*
SIGTERM is the termination signal.
The default behavior is to terminate the process, but it also can be caught or ignored.
The intention is to kill the process, gracefully or not, but to first allow it a chance to cleanup.
*/
// Terminate the child process (the child can prevent this)
kill(child, SIGTERM);
/*
SIGINT is the interrupt signal.
The terminal sends it to the foreground process when the user presses ctrl-c.
The default behavior is to terminate the process, but it can be caught or ignored.
The intention is to provide a mechanism for an orderly, graceful shutdown.
*/
// Equivalent to CTRL-C (by default closes the process)
kill(child, SIGINT);
/*
SIGKILL is the kill signal.
The only behavior is to kill the process, immediately.
As the process cannot catch the signal, it cannot cleanup, and thus this is a signal of last resort.
*/
kill(child, SIGKILL);
As we saw above there is also a kill command available in the shell e.g. get a list of running processes and then terminate process 45 and process 46
ps
kill -l
kill -9 45
kill -s TERM 46
On a Linux system, see man -s7 signal
if you are interested in finding out more (for example a list of system and library calls that are async-signal-safe.
There are strict limitations on the executable code inside a signal handler. Most library and system calls are not 'async-signal-safe'
- they may be not use used inside a signal handler because they are not re-entrant safe. In a single-threaded program, signal handling momentarily interrupts the program execution to execute the signal handler code instead. Suppose your original program was interrupted while executing the library code of malloc
; the memory structures used by malloc will not be in a consistent state. Calling printf
(which uses malloc
) as part of the signal handler is unsafe and will result in "undefined behavior" i.e. it is no longer a useful,predictable program. In practice your program might crash, compute or generate incorrect results or stop functioning ("deadlock"), depending on exactly what your program was executing when it was interrupted to execute the signal handler code.
One common use of signal handlers is to set a boolean flag that is occasionally polled (read) as part of the normal running of the program. For example,
int pleaseStop ; // See notes on why "volatile sig_atomic_t" is better
void handle_sigint(int signal) {
pleaseStop = 1;
}
int main() {
signal(SIGINT, handle_sigint);
pleaseStop = 0;
while ( ! pleaseStop) {
/* application logic here */
}
/* cleanup code here */
}
The above code might appear to be correct on paper. However, we need to provide a hint to the compiler and to the CPU core that will execute the main()
loop. We need to prevent a compiler optimization: The expression ! pleaseStop
appears to be a loop invariant i.e. true forever, so can be simplified to true
. Secondly, we need to ensure that the value of pleaseStop
is not cached using a CPU register and instead always read from and written to main memory. The sig_atomic_t
type implies that all the bits of the variable can be read or modified as an "atomic operation" - a single uninterruptable operation. It is impossible to read a value that is composed of some new bit values and old bit values.
By specifying pleaseStop
with the correct type volatile sig_atomic_t
we can write portable code where the main loop will be exited after the signal handler returns. The sig_atomic_t
type can be as large as an int
on most modern platforms but on embedded systems can be as small as a char
and only able to represent (-127 to 127) values.
volatile sig_atomic_t pleaseStop;
Two examples of this pattern can be found in "COMP" a terminal based 1Hz 4bit computer (https://github.com/gto76/comp-cpp/blob/1bf9a77eaf8f57f7358a316e5bbada97f2dc8987/src/output.c#L121).
Two boolean flags are used. One to mark the delivery of SIGINT
(CTRL-C), and gracefully shutdown the program, and the other to mark SIGWINCH
signal to detect terminal resize and redraw the entire display.