Skip to content

The purpose of this project is to code a small data exchange program using UNIX signals.

Notifications You must be signed in to change notification settings

pvaladares/42cursus-02-minitalk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Table of Contents

1 - Goal

The purpose of this project is to code a small data exchange program using UNIX signals.

2 - Analysing the subject

2.1 - Requirements

You must create a communication program in the form of a client and a server.

  • The server must be started first. After its launch, it has to print its PID.
  • The client takes two parameters: 1) The server PID 2) The string to send.
  • The client must send the string passed as a parameter to the server. Once the string has been received, the server must print it.
  • The server has to display the string pretty quickly. Quickly means that if you think it takes too long, then it is probably too long.
  • Your server should be able to receive strings from several clients in a row without needing to restart.
  • The communication between your client and your server has to be done only using UNIX signals.
  • You can only use these two signals: SIGUSR1 and SIGUSR2.

2.2 - Note #1 - Display a string, not char by char

To be noted that as per the subject, it cannot be displayed char-by-char on the server-side, it must be displayed the whole message received instead:

Once the string has been received, the server must print it.

Therefore, it must be known in advance the length of the message so the server can allocate memory in the heap as required only once (best approach). Or alternatively, don't send the lenght and keep modifying the allocation in memory - joining a char to already received string - till receiving the last char (not so good approach since will have to repeat allocation for every new char received). The following functions from libft could be used:

  • ft_strlen(), to know the length of the message to be sent from client to server
  • ft_calloc(), in order to save the chars being received till the string is completed received on server side, so it can be then displayed

2.3 - Note #2 - Communication between server and client (two way channel)

  • The server has to display the string pretty quickly. Quickly means that if you think it takes too long, then it is probably too long.

Researching about the theme, two options arise:

  • using a delay function like sleep() or usleep() avoiding the communication of signals back and forward between server/client, which would impose a delay per char, since we would be sending in blind way (no feedback loop) chars from client to server. Depending on the delay set signals could queue.

  • the other option would be to implement a feedback loop so whenever client sends a char to server, it waits till server sends back a ACK signal informing client that the next bit can be sent. This later solution should be a lot quicker since there would be no delay function. This solution is aligned with the bonus: The server acknowledges every message received by sending back a signal to the client.

As noted on the subject, there should not be sent signals blindly, making the two-way communication the best implementation.

Linux system does NOT queue signals when you already have pending signals of this type! Bonus time?

3 - Implementation

3.1 - Allowed functions

3.1.1 - Libft

The subject states that libft library can be used!

You can definitely use your libft.

Reading the subject it can be understood some functions included in the libft will be required to fulfill the requirements, as shown below:

The server must be started first. After its launch, it has to print its PID.

  • The client takes two parameters:
    • The server PID.
    • The string to send.
  • ft_atoi(), to convert the PID argument received from command line to integer type for further processing of signal processing

3.1.2 - Other functions

Below is made a brief analysis of other functions that can be used.

In order to complete the mandatory part, you are allowed to use the following functions:

  • write
  • ft_printf and any equivalent YOU coded
  • signal
sig_t signal(int sig, sig_t func);
This signal() facility is a simplified interface to the more general sigaction(2) facility.
(...)
some signals instead cause the process receiving them to be stopped, or are simply discarded
if the process has not requested otherwise.  
Except for the SIGKILL and SIGSTOP signals, the signal() function allows for a signal to be 
caught, to be ignored, or to generate an interrupt.
These signals are defined in the file <signal.h>:

           Name             Default Action       Description
     1     SIGHUP           terminate process    terminal line hangup
     2     SIGINT           terminate process    interrupt program 
(...)
     30    SIGUSR1          terminate process    User defined signal 1
     31    SIGUSR2          terminate process    User defined signal 2  

The sig argument specifies which signal was received.  
The func procedure allows a user to choose the action upon receipt of a signal.

The handled signal is unblocked when the function returns and the process continues 
from where it left off when the signal occurred.

For some system calls, if a signal is caught while the call is executing and the call is 
prematurely terminated, the call is automatically restarted.  
Any handler installed with signal(3) will have the SA_RESTART flag set, 
meaning that any restartable system call will not return on receipt of a signal.  

The affected system calls include read(2), write(2), sendto(2), recvfrom(2), sendmsg(2), 
and recvmsg(2) on a communications channel or a low speed device and during a ioctl(2) or wait(2).
However, calls that have already committed are not restarted, but instead return a partial success
(for example, a short read count).  
These semantics could be changed with siginterrupt(3).
     
See sigaction(2) for a list of functions that are considered safe for use in signal handlers.

The previous action is returned on a successful call.  
Otherwise, SIG_ERR is returned and the global variable errno is set to indicate the error.
     
macOS 12.4                      June 7, 2004                      macOS 12.4
  • sigemptyset
  • sigaddset
int sigemptyset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
These functions manipulate signal sets, stored in a sigset_t.
sigemptyset() must be called for every object of type sigset_t before any other use of the object.

The sigemptyset() function initializes a signal set to be empty.

The sigaddset() function adds the specified signal signo to the signal set.

These functions are provided as macros in the include file <signal.h>.
Actual functions are available if their names are undefined (with #undef name).

Functions return 0.

macOS 12.4          June 4, 1993          macOS 12.4
  • sigaction
#define sa_handler      __sigaction_u.__sa_handler
#define sa_sigaction    __sigaction_u.__sa_sigaction

int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);
The system defines a set of signals that may be delivered to a process.
Signal delivery resembles the occurrence of a hardware interrupt: 
the signal is normally blocked from further occurrence, the current process
context is saved, and a new one is built.  
A process may specify a handler to which a signal is delivered, or specify that 
a signal is to be ignored.
A process may also specify that a default action is to be taken by the
system when a signal occurs.  
A signal may also be blocked, in which case its delivery is postponed until it is unblocked.  
The action to be taken on delivery is determined at the time of delivery.  
Normally, signal handlers execute on the current stack of the process.  
This may be changed, on a per-handler basis, so that signals are taken on a special signal stack.

The sigaction() system call assigns an action for a signal specified by sig.  
If act is non-zero, it specifies an action (SIG_DFL, SIG_IGN, or a handler routine) and mask 
to be used when delivering the specified signal.
If oact is non-zero, the previous handling information for the signal is returned to the user.

Options may be specified by setting sa_flags.  The meaning of the various bits is as follows:

           (...)

           SA_NODEFER      If this bit is set, further occurrences of the
                           delivered signal are not masked during the execution
                           of the handler.

           SA_RESETHAND    If this bit is set, the handler is reset back to
                           SIG_DFL at the moment the signal is delivered.

           SA_RESTART      See paragraph below.
           
           SA_SIGINFO      If this bit is set, the handler function is assumed
                           to be pointed to by the sa_sigaction member of struct
                           sigaction and should match the prototype shown above
                           or as below in EXAMPLES.  This bit should not be set
                           when assigning SIG_DFL or SIG_IGN.
                           
If a signal is caught during the system calls listed below, the call may be
forced to terminate with the error EINTR, the call may return with a data
transfer shorter than requested, or the call may be restarted.  
Restart of pending calls is requested by setting the SA_RESTART bit in sa_flags.  
The affected system calls include (...), write(2) and (...) on a communications channel or a
slow device (such as a terminal, but not a regular file) and during a wait(2) or (...).  
However, calls that have already committed are not restarted, but instead return a partial success 
(for example, a short read count).

NOTE
     The sa_mask field specified in act is not allowed to block SIGKILL or
     SIGSTOP.  Any attempt to do so will be silently ignored.

     The following functions are either reentrant or not interruptible by
     signals and are async-signal safe.  Therefore applications may invoke them,
     without restriction, from signal-catching functions:

     Base Interfaces:

     (...), getpid(), (...), kill(), (...), pause(), (...), sigaction(), sigaddset(), (...),
     sigemptyset(), (...), signal(), (...), sleep(), (...), wait(), (...), write().

     Realtime Interfaces:

     aio_error(), sigpause(), aio_return(), aio_suspend(), sem_post(), sigset().

     ANSI C Interfaces:

     strcpy(), strcat(), strncpy(), strncat(), and perhaps some others.
     
     Extension Interfaces:

     strlcpy(), strlcat().

     All functions not in the above lists are considered to be unsafe with
     respect to signals.  That is to say, the behaviour of such functions when
     called from a signal handler is undefined.  In general though, signal
     handlers should do little more than set a flag; most other actions are not
     safe.

     Also, it is good practice to make a copy of the global variable errno and
     restore it before returning from the signal handler.  This protects against
     the side effect of errno being set by functions called from inside the
     signal handler.
  • kill

Using kill() to check the PID server input

DESCRIPTION
     The kill() function sends the signal specified by sig to pid, a process or
     a group of processes.  Typically, Sig will be one of the signals specified
     in sigaction(2).  A value of 0, however, will cause error checking to be
     performed (with no signal being sent).  This can be used to check the
     validity of pid.

This is a nice finding to check the validity of the PID in a robust way, rather than just checking if all the chars are digits.

else if (kill(ft_atoi(argv[1]), 0) < 0)
{
	ft_putstr_fd("\e[31m## error - PID is invalid ##\n\e[0m", STDOUT_FILENO);
	return (EXIT_FAILURE);
}
     If pid is greater than zero:
             Sig is sent to the process whose ID is equal to pid.

     If pid is zero:
             Sig is sent to all processes whose group ID is equal to the process
             group ID of the sender, and for which the process has permission;
             this is a variant of killpg(2).

     If pid is -1:
             If the user has super-user privileges, the signal is sent to all
             processes excluding system processes and the process sending the
             signal.  If the user is not the super user, the signal is sent to
             all processes with the same uid as the user, excluding the process
             sending the signal.  No error is returned if any process could be
             signaled.
  • getpid
pid_t getpid(void);
DESCRIPTION
     getpid() returns the process ID of the calling process.  The ID is
     guaranteed to be unique and is useful for constructing temporary file
     names.
  • malloc
  • free
  • pause
int pause(void);
DESCRIPTION
     The pause() function causes the calling thread to pause until a signal is
     received from either the kill(2) function or an interval timer.  (See
     setitimer(2).) Upon termination of a signal handler started during a
     pause(), the pause() call will return.
  • sleep
  • usleep
int usleep(useconds_t microseconds);
DESCRIPTION
     The usleep() function suspends execution of the calling thread until either
     microseconds have elapsed or a signal is delivered to the
     thread and its action is to invoke a signal-catching function or to
     terminate the process.  System activity or limitations may lengthen the
     sleep by an indeterminate amount.

     This function is implemented using nanosleep(2) by pausing for microseconds
     microseconds or until a signal occurs.  Consequently, in this
     implementation, sleeping has no effect on the state of process timers, and
     there is no special handling for SIGALRM.  Also, this implementation does
     not put a limit on the value of microseconds (other than that limited by
     the size of the useconds_t type); some other platforms require it to be
     less than one million.
  • exit
DESCRIPTION
     The exit() function terminates a process.

     Before termination, exit() performs the following functions in the order
     listed:

           1.   Call the functions registered with the atexit(3) function, in
                the reverse order of their registration.

           2.   Flush all open output streams.

           3.   Close all open streams.

           4.   Unlink all files created with the tmpfile(3) function.

     Function make the low-order eight bits of the status argument available to 
     a parent process which has called a wait(2)-family function.
     
     The C Standard (ISO/IEC 9899:1999 (“ISO C99”)) defines the values 0,
     EXIT_SUCCESS, and EXIT_FAILURE as possible values of status.

4 - Testing

4.1 - Testers

4.2 - Result

minitalk francinette result

5 - Demo

minitalk demo

  • Below is a demonstration of the usage of the server and client programs
    • Erro handling, e.g. incorrect number of arguments and invalid PID server
    • Sending empty messages with "" and '' - transmission of empty message is received by server (length 0) and displayed
    • Sending Large message sent +1000 chars using Lorem Ipsum site
    • Receiving a message from a 2nd client after 1st client (in a row)
    • Client showing transmission log

6 - Resources

Releases

No releases published

Packages

No packages published