Build a Server – Client Application in C that executes concurrently Terminal Commands

In this post we will build a server – client application that will get our hands dirty with signals, fork, named pipes and the exec command. This application has a very simple concept:

“Client creates, with a new process, the Server and sends to him terminal commands using as interprocess communication a named pipe (aka fifo pipe)”

Moreover this model must executes terminal commands concurrently and we accomplish that using a signal handler. To be more specific when a client sends through the named pipe a command, the server wakes up because the signal handler  receives the SIGCONT signal and changes the wake up variable from 1 to 0. This wake up variable is being used in the server to keep him in sleep mode and not busy waiting. Let’s see a simple example with just the mechanism of it:

Server:

void catch(int signo)//function to wake up server when it has no job to do and it sleeps
{
printf("Catching signal...\n");
wakeup=0;
}

int server(int argc, char *argv[])
{

int wakeup;

while(1)
{

wakeup=1;
while(wakeup) //sleeping loop of server
{
sleep(5);
}

//manage incoming command from client


}
return 0;

Client:

int client(int argc, char *argv[])
{
   //create command
   //get server's process id
kill(server_pid, SIGCONT); //send signal to server

return 0;
}

Some  crucial notes here:

  • Client (or commander as it is appears in the code at the end of this post) does his own job and terminates. That means that every time we call commander to ask something from server (e.x. how many processes are running, to stop a specific command or to run a new one) he must find out if the server exists. If server exists commander will read successfully his process id from, a dedicated to this job, text file that server writes it’s own pid when he gets born.
  • It isn’t necessary to send a SIGCONT signal to server. You can send any signal and for this use server will wake up as we want to.
  • It isn’t necessary to use a fifo – pipe. Simple pipes do the work also but compared with fifo pipes they have to start the reading/writing processes at the same time and you cant have multiple readers/writes that do not need common ancestry.

So when our server receives the signal and is getting woken up the first thing that he does is to read the command from client. This is being done by firstly reading the length of the command and then reading the command. After we read the command from the named pipe we cut the string into pieces of arguments (e.x. command issue ls -l breaks into three arguments issue, ls and -l) and check the first argument to decide what client requests (in the previous example client requests to run a new command “ls -l”).

The important part of the application is when client requests to run a new command or to change the number of the commands that can run concurrently. If we understand these methods the rest is the easy part. The concept of issuing a new command is this:

  1. Create a unique identifier (job_id)  for the new command
  2. If our list with the commands that run is full put the new command into the waiting queue
  3. Else if we have space insert the information of the new command into running list, create a new process with fork and execute it.

Here is a simple snippet of the above method (run_process) and of the structure of waiting queue (queue2) and running list (queue1):

Server:

int server(int argc, char *argv[])
{

int wakeup;

while(1)
{

wakeup=1;
while(wakeup) //sleeping loop of server
{
sleep(5);
}

read(readfd, &length, sizeof(int));//read length of command
read(readfd, command, length);//read command


command[strlen(command)] = '\0';

//break command into arguments
extract_command(ext_command, command, length, &args);

if(strcmp(ext_command[0], "issue") == 0)//check what client requests
{
run_process(command, ext_command, args, 1, -1);
}
//if clients wants to change concurrency change the global variable N which is responsible for 
//how many commands can run at the same time
else if(strcmp(ext_command[0], "setConcurrency") == 0)
{
N = atoi(ext_command[1]);
manage_process(queue1, queue2);
}

}

return 0;

Run Process(Semi – Pseudocode):

void run_process(char command[1000], char ext_command[20][100], int args)
{
    int i, childpid;
    char **arg;
   
    job_id++;//unique identifier for each process
        
    
    if(number_elements(queue1, &q1) >= N)//put job into waiting queue
    {
        insert(queue2, job_id, -1, command, ext_command, args, &q2);//inserting job to waiting queue 
    }    
    else//create a new process and run the given command
    {
        childpid = fork();

        if(childpid == 0)//child
        {    
            //if we have space for another process
            if(number_elements(queue1, &q1) < N)
            {
                arg = malloc(args*sizeof(char*));
                for (i = 0; i < args; i++)//fixing arguments for 
                {                          //exec
                    arg[i] = malloc(30*sizeof(char));
                    strcpy(arg[i], ext_command[i+1]);
                }
                arg[args]='\0';
                
          
                execvp(arg[0], arg);
            }
        
            for (i = 0; i<args; i++)//free memory
                free(arg[i]);
            free(arg);    
            return;
        }
        

        insert(queue1, job_id, childpid, command, ext_command, args, &q1);//inserting job to running queue
       
    }
}


<strong>Queue and List nodes:</strong>
struct counter {
    int front;
    int rear;
};

struct node {
    int pid; //this field gets a value only in running list
    int job_id; //unique identifier for each waiting or running command
    char command[100]; //raw string of the command
    char ext_command[20][100]; //broken into arguments command (extracted)
    int args; //number of the arguments
};

Notes:

  • ext_command is the command broken into arguments as I mentioned previously
  • command is the whole string as it was typed from client
  • arg is being used to pass the parameters to the exec function that roughly runs a program (system call or a program of ours upon a process)
  • childpid is the return value of fork and if is 0 we are in the section of child otherwise we are in the section of parent (for those who are not familiar with the fork function I suggest you check it here.

If you got the idea of how the above code works you should wonder what manage_process function does.

This method simply transfers any waiting commands from waiting queue to running list. This happens if we change the concurrency variable for example from 3 to 5. Then if we have any waiting command in waiting queue it will be put immediately into running queue and of course execution.

Here’s the simple code of this method:

Manage Process:

void manage_process(Queue queue1[MAX], Queue queue2[MAX])//function that migrates processes from waiting to running mode
{
//keep running waiting processes until you reach concurrency limit
while((number_elements(queue2, &q2) > 0) && (number_elements(queue1, &q1) < N))
{
struct node proc;

delete(queue2, &proc, &q2);//delete command from waiting queue

run_process(proc.command, proc.ext_command, proc.args, 2, proc.job_id);//transfer it to running list
}

}

 

Click here to download the complete implementation of the jobExecutor and if you have any questions feel free to post in the comment section below!

To run the application execute the jobexecutor server and then execute the jobcommander with the following sample commands:

./jobcommander issue ls -a

./jobcommander setConcurrency 4

./jobcommander issue ls

./jobcommander stop 2 (this command will terminat issue ls)

./jobcommander exit

If you want to test the application on concurrency you should better make a small bash script.

Enjoy and have fun!

 



Giannis Kanellopoulos

Giannis Kanellopoulos

Biography to be completed

More Posts