0% found this document useful (0 votes)
20 views6 pages

Understanding Signals in Network Programming

A signal is a message sent to a process that can interrupt execution. Common signals include SIGINT (Ctrl-C), SIGSEGV (invalid memory access), and SIGCHLD (child process terminated). Processes can install signal handlers to catch signals or ignore them. When a child process exits, the parent receives a SIGCHLD signal. If not handled properly, the child becomes a zombie process. The wait() and waitpid() functions allow the parent to reap the child's exit status and avoid zombies. Some system calls like accept() need to be restarted if interrupted by a signal to avoid errors.

Uploaded by

taleysha vuyanzi
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
20 views6 pages

Understanding Signals in Network Programming

A signal is a message sent to a process that can interrupt execution. Common signals include SIGINT (Ctrl-C), SIGSEGV (invalid memory access), and SIGCHLD (child process terminated). Processes can install signal handlers to catch signals or ignore them. When a child process exits, the parent receives a SIGCHLD signal. If not handled properly, the child becomes a zombie process. The wait() and waitpid() functions allow the parent to reap the child's exit status and avoid zombies. Some system calls like accept() need to be restarted if interrupted by a signal to avoid errors.

Uploaded by

taleysha vuyanzi
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

SIGNALS

A signal is a message (an integer) sent to a process. Signals are sometimes called software interrupts. The
receiving process can try to ignore the signal or call a routine (signal handler). After returning from the signal
handler, the receiving process will resume execution at the point at which it was interrupted. The system-calls
that deal with signals vary between one Unix version and another

The following conditions that can generate a signal():


• When user presses terminal keys, the terminal will generate a signal. For example, when the
user breaks a program by CTRL + C key pair.
• Hardware exceptions can generate signals e.g. division by 0, invalid memory reference etc.
Inexperienced programmers often get SIGSEGV (Segmentation Violation signal) because of
an invalid address in a pointer.
• Processes can send signals to themselves by using kill() system call (If permissions allow).
• Kernel can generate signal to inform processes when something happens. For example, SIGPIPE
will be generated when a process writes to a pipe which has been closed by the reader.

Signals can be sent using the kill()routine. The signal()and sigaction() routines are used to
control how a process should respond to a signal.

Posix Signal Handling:


Every signal has a disposition, which is called the action associated with the signal. The disposition is set
by calling the sigaction function.

There are three choices for the disposition:


i) Whenever a specific signal occurs, a specific function can be provided. This function is called signal
handler and the action is called catching the signal. The two signal SIGKILL and SIGSTOP cannot be
caught – this is an exception.
• The function is called with a single integer argument that is the signal number and the function
returns nothing as shown below:

void handler (int signo);

• For most signals, calling sigaction and specifying a function to be called when the signal occurs
is all that is required to catch the signal.
• For few signal like SIGIO, SIGPOLL, and SIGURG, all require additional actions on the part of the
process is required to catch the signal.
ii) A signal can be ignored by setting its disposition to SIG_IGN. Again the two signals SIGKILL and
SIGSTOP are exceptions and cannot be ignored
iii) We can set the default disposition for a signal by setting its disposition to SIG_DFL. The default is
normally to terminate a process on the receipt of a signal, with certain signal also generating a core image
of the process in its current working directory. The signals whose default disposition is to be ignored are:
SIGCHLD and SIGURG .

The signal Function:


Posix way to establish the disposition of a signal is to call the sigaction function. However this is
complicated as one argument to the function is a structure that must be allocated and filled in. An easier
way to set the disposition for a signal is to call the signal function. The first argument is the signal
name and the second arguments is the pointer to a function or one of the constants SIG_IGN or
SIG_DFE. Normally, you can define your own signal function that just calls the Posix sigaction. This
provides a simple interface with the desired POSIX semantics.

Network Programming ~ Wainaina Page 1 of 6


#include "unp.h"
Sigfunc * signal (int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset (&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif
}
else
{
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; /* SVR4, 4.4BSD */
#endif
}
if (sigaction (signo, &act, &oact) < 0)
return (SIG_ERR);
return (oact.sa_handler);
}

A call to the function when a signal occurs. The function has pointer to signal handling function as the second
argument. The sa_mask member to the empty set, which means that no additional signals will be blocked
while the signal handler is running.
SA_RESTART is an optional flag. When the flag is set, a system call interrupted by this signal will be
automatically restarted by the kernel
The function sigaction is called and then return the old action for the signal as the return value of the signal
function.

Handling SIGCHLD signals


A zombie is a process that has terminated and whose parent is still running, but has not yet waited for its child
processes. This will result in the resources occupied by the terminated process not to be returned to the system.
If there are a lot of zombies in the system the system resources may run out.
The purpose of the zombie state is to maintain information about the child for the parent to fetch at some time
later. The information are: the process ID of the child, its termination status and information on the resource
utilization of the child (CPU time, memory etc). If a process terminates, and it has children in the zombie state
then the parent process ID of all the zombie children is set to 1 ( init process), which will inherit the children
and clean them up i.e., init will wait for them, which removes the zombie

Handling Zombies
Whenever you fork children, you must wait, for them to prevent them from becoming zombies.
When a child terminates, the kernel generates a SGICHLD signal for the parent. The parent process should
catch the signal and take an appropriate action. If the signal is not caught, the default action of this signal is to
be ignored.
To handle a zombie, the parent process establishes a signal handler to catch SIGCHLD signal. The handler then
invokes the function wait() or waitpid() to wait for a terminated child process and free all the system
resources occupied by the process. You can establish the signal handler by adding the following function call

signal (SIGCHLD, sig_chld);

This is after a call to listen function and it must be done sometime before you fork the first child and
needs to be done only once. You then need to define the signal handler, the function sig_chld, as follows:
Network Programming ~ Wainaina Page 2 of 6
#include "unp.h"
void sig_chld(int signo)
{
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminated\", pid);
return;
}

Interrupted System Calls


There are certain system calls in the Client Server communication that are blocked while waiting for input. E.g.
in the case of echo client server program, in the server program, the function accept() is blocked while
waiting for the call from a client. This condition is called slow system call i.e. a system call that never return.
Under such condition, when a SIGCHLD is delivered on termination of a process, the sig_chld function
executes, wait function fetches the child‘s PID and termination status. The signal handler then returns.
But as the signal was caught by the parent while parent was blocked in a slow system call (accept), the
kernel causes the accept to return an error of EINTR (interrupted system call). The parent does not handle
this error, so it aborts. This is a potential problem which all slow system call (read, write, open, select
etc) face whenever they catch any signal and which is undesirable.

Handling the Interrupted System Calls


The basic rule that applies here is that when a process is blocked in a slow system call and the process catches a
signal and the signal handler returns, the system call can return an error of EINTR. To handle interrupted
accept, you need to change the call to accept (server program) in the following way:

for ( ; ; ) {
clilen = sizeof (cliaddr);
if ( (connfd = accept (listenfd, (SA *) &cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* back to for () */
else
err_sys ("accept error");
}

In this code, the interrupted system call is restarted. This method works for the functions read, write, select and
open etc. But there is one function that cannot be restarted by itself. – connect. If this function returns
EINTR, you cannot call it again, as doing so will return an immediate error. In this case we must call select
to wait for the connection to complete.

The wait () and waitpid () functions


#include <sys/wait.h>
pid_t wait (int *statloc);
pid_t waitpid (pid_t pid, int *statloc, int options);

Both return: process ID if OK, 0 or 1 on error

wait and waitpid both return two values: the return value of the function is the process ID of the terminated
child, and the termination status of the child (integer) is returned through statloc pointer.
If there are no terminated children for the calling wait, but the process has one or more children that are still
executing, then wait blocks until the first of the existing children terminate.
Network Programming ~ Wainaina Page 3 of 6
waitpid gives more control over which process to wait for and whether or not to block. pid argument specify
the process id that you want to wait for. A value of -1 says to wait for the first of the children to terminate. The
option argument lets you specify additional options. The most common option is WNOHANG. This tells the
kernel not to block if there are no terminated children.

Difference between wait and waitpid


If the client establishes five connections with the server and then uses only the first one ( sockfd[0]) in the call
to str_cli. The purpose of establishing multiple connections is to spawn multiple children from the concurrent
server, as shown below

The TCP / IP client program is modified as follows:

#include "unp.h"
int main (int argc, char **argv)
{
int i, sockfd[5];
struct sockaddr_in servaddr;
if (argc != 2)
err_quit ("usage: tcpcli <IPaddress>";
for (i = 0; i < 5; i++) {
sockfd[i] = Socket (AF_INET, SOCK_STREAM, 0);
bzero (&servaddr, sizeof (servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons (SERV_PORT);
inet_pton (AF_INET, argv[1], &servaddr.sin_addr);
connect (sockfd[i], (SA *) &servaddr, sizeof (servaddr));
}
str_cli (stdin, sockfd[0]); /* do it all */
exit(0);
}

When the client terminates, all open descriptors are closed automatically by the kernel and all the five
serve children terminate at about the same time. This causes five SIGCHLD signals to be delivered to the
parent at about the same time, as shown below:

Network Programming ~ Wainaina Page 4 of 6


It is this delivery of multiple occurrences of the same signal that causes the problem i.e. establishing a
signal handler and calling wait from the signal handler are insufficient for preventing zombies.
The problem is that all five signals are generated before the signal handler is executed, and the signal
handler is executed only one time because UNIX signals are not normally queued.
The correct solution is to call waitpid instead of wait. The code below shows the server version of
the sig_chld function that handles SIGCHLD. This version works because you call waitpid within
the loop, fetching the status of any of the children that has terminated. You must also specify the
WNOHNG option; This tells waitpid not to block if there exists running children that not yet terminated.
In the code for wait , you cannot call wait in a loop, because there is no way to prevent wait from
blocking if there are running children that have not yet terminated. This version correctly handles a
return EINTR from accept and it establishes a signal handler that called waitpid for all terminated
children.

The following program shows the implementation of waitpid(). The second part is the
implementation of complete server program incorporating the signal handler.

#include "unp.h"
void sig_chld(int signo)
{
pid_t pid;
int stat;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("child %d terminated\n", pid);
return;
}

The correct version of TCP server that handles an error of EINTR from accept.

#include "unp.h"
int main(int argc, char **argv)
{
int listenfd, connfd;
pid_t childpid;
socklen_t clilen;
struct sockaddr_in cliaddr, servaddr;
void sig_chld(int);

listenfd = Socket (AF_INET, SOCK_STREAM, 0);


bzero (&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
listen(listenfd, LISTENQ);
signal(SIGCHLD, sig_chld); /* must call waitpid() */
for ( ; ; ) {
clilen = sizeof(cliaddr);
if ( (connfd = accept (listenfd, (SA *) &cliaddr, &clilen)) < 0)
{
if (errno == EINTR)
continue; /* back to for() */
else
err_sys("accept error");
}
if ((childpid = Fork()) == 0) { /* child process */
close(listenfd); /* close listening socket */
str_echo(connfd); /* process the request */
Network Programming ~ Wainaina Page 5 of 6
exit(0);
}
close (connfd); /* parent closes connected socket */
}
}

The three scenarios that you encounter with networking program are:
i) We must catch the SIGCHLD signal when forking child processes.
ii) We must handle interrupted system calls when we catch signals.
iii) A SIGCHLD handler must be coded correctly using waitpid to prevent any zombies from being
left around.

Network Programming ~ Wainaina Page 6 of 6

Common questions

Powered by AI

Using waitpid instead of wait is recommended due to its non-blocking nature and control it offers over process handling. In situations where multiple child processes may terminate simultaneously, as in concurrent server environments, waitpid with the WNOHANG option allows a parent process to check the status of specific child processes without blocking if none have terminated yet. This is crucial when multiple SIGCHLD signals are received before the handler is executed, which can cause missed signals if using wait. Waitpid processes children as they become zombies, efficiently releasing resources without blocking the execution .

Handling SIGCHLD signals is crucial to prevent zombie processes, which occur when a child process terminates but its parent hasn't called wait() or waitpid() to retrieve its status. By setting a signal handler for SIGCHLD, the parent can catch this signal and call either wait() or waitpid() to properly terminate child processes. Waitpid is preferable as it provides more control, such as the use of WNOHANG option to avoid blocking if there are no terminated children. This ensures that terminated children's resources are released back to the system, preventing zombies .

Handling multiple SIGCHLD signals in a concurrent server environment presents challenges due to the non-queuing nature of signals in Unix, leading to missed signals when multiple child processes terminate nearly simultaneously. Implementing SIGCHLD handlers with wait() can be inefficient, as subsequent child terminations might not be captured. The solution is to use waitpid() in a loop with WNOHANG, ensuring each terminated child is properly accounted for without blocking, accommodating high concurrency levels typical in TCP/IP servers. This strategy effectively manages signal-induced race conditions and ensures robust cleanup of zombie processes .

Interrupted system calls, such as accept(), occur in client-server communication when a syscall is blocked, waiting for an external event, and is disrupted by a signal like SIGCHLD from a terminated child. The syscall then returns an EINTR error. This can cause the server to abort if the error is not properly handled. To address this, the syscalls should be wrapped in a loop that re-invokes them upon encountering EINTR, effectively ignoring the interruption and resuming regular execution. This approach applies to other blocking calls like read, write, and select, ensuring robust communication .

Signal handlers in Unix systems are used to manage and respond to signals, which are messages sent to processes. They serve several purposes: a) managing hardware exceptions, like division by zero, which generate signals like SIGFPE; b) handling user interruptions (e.g., pressing CTRL+C) which send signals like SIGINT; c) implementing user-defined responses to certain events or process interruptions by using functions such as signal() or sigaction(). Handling involves setting a signal disposition which could be one of three: executing a custom function, ignoring the signal, or executing a default action, often terminating the process. However, signals like SIGKILL and SIGSTOP cannot be caught or ignored .

In Unix systems, dealing with persistent zombie states involves implementing signal handlers for SIGCHLD signals to call wait() or waitpid() on terminated child processes. By defining a handler, like sig_chld, and associating it with SIGCHLD, the parent process can proactively manage child termination, immediately reclaiming resources. In practice, the handler should use waitpid() within a loop and the WNOHANG option, to handle multiple, simultaneous terminations without blocking. This ensures that all terminated children are properly waited for and cleaned up, preventing resource leakage .

The signal() function simplifies signal handling by providing a straightforward way to set a signal's disposition, either to a custom handler or to default actions (SIG_IGN, SIG_DFL). It abstracts the complexity of setting up sigaction by allowing less detailed configurations, making it appealing for quick or less advanced use cases. However, sigaction() is preferred in scenarios where more control is needed, such as setting the signal mask, specifying flags like SA_RESTART, or when dealing with historical behaviors across different Unix systems; it provides a consistent POSIX-compliant interface which might be crucial for handling complex signal interactions appropriately .

Signal masking during the execution of a signal handler refers to blocking additional signals from being delivered while the signal handler is executing. This is set using the sa_mask field in the sigaction structure. The significance of signal masking is to prevent reentrant issues where the same signal could be handled multiple times concurrently if a handler were interrupted by the same or another signal. This ensures that the current signal handler has exclusive access to resources or operations it might be using, maintaining program reliability and avoiding potential deadlocks or race conditions .

Without proper queuing, Unix systems may overwrite signals of the same type if they arrive before the previous is handled, leading to signal loss. This is particularly risky in complex programs where signal-induced operations are critical, such as in servers managing numerous child processes. The mitigation involves using waitpid() in a handler loop with WNOHANG to periodically check for any child process termination rather than relying solely on single signal instances. Additionally, utilizing appropriate synchronization mechanisms can prevent race conditions and ensure all signals are processed accordingly, preserving system reliability .

In Unix systems, the SA_RESTART flag allows for system calls that are interrupted by signals to be automatically retried rather than failing with an EINTR error. The usage of SA_RESTART varies across Unix variants; systems supporting this flag can offer more seamless operations by hiding interruptions from the application, continuing blocked operations without developer intervention. Without this flag, applications must manually check for EINTR and loop to retry the call, complicating the development of robust error handling mechanisms. The choice affects system call reliability and error handling complexity significantly .

You might also like