Skip Navigation | ANU Home | Search ANU | Search FEIT | Feedback
The Australian National University
Faculty of Engineering and Information Technology (FEIT)
Department of Computer Science
Printer Friendly Version of this Document
High Performance Scientific Computing COMP2310

Laboratory 9

Signals and UNIX Semaphores


You are expected to use the man pages! Remember that man pages with the same title may appear in multiple sections. If you want to search all sections use man -a.


Signals


A signal is like a message that when delivered to a process may cause it to halt what it is currently doing and execute some alternative set of instructions. We say "may" because a process can elect to ignore certain signals. Each signal is characterized by an integer value, with some values pre-assigned. For instance you may be familiar with the signal used to kill a running process. This signal is known as SIGKILL, has a value of 9, and can be sent to a particular process ID (PID) by issuing the command kill -9 PID. Details of other predefined signals are available from the man pages by typing man 7 signal.

A process may control the effects of signals by installing a signal handler function. In this sense, signals are similar to the traps that you used with PeANUt in COMP2300 (however, signals are not to be confused with traps or interrupts, as they are purely implemented by the software and OS).

In the following code we create two signal handlers. Using the function signal(), we assign these to be executed when our executing process receives either SIGUSR1 or SIGUSR2. We then put our program into an infinite number of 1 second sleeps.

    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>
    void handler1(int sig);
    void handler2(int sig);
    int main(int argc, char* argv[]) {
    
      /* Print out process ID and signal values */
      printf("Hello from Process ID %d \n", getpid());
      printf("Value of SIGUSR1 %d \n", SIGUSR1);
      printf("Value of SIGUSR2 %d \n", SIGUSR2);
    
      /* Assign the signal handlers*/
      signal(SIGUSR1, handler1);
      signal(SIGUSR2, handler2);
    
      while (1) sleep(1);
    
      return 0;
    }
    
    void handler1(int sig) {
      printf("Handler 1 caught signal %d\n", sig);
      return;
    }
    
    void handler2(int sig) {
      printf("Handler 2 caught signal %d\n", sig);
      return;
    }
    

ACTIONS

  • Compile and run the above program. Test whether the signal handler is working by sending the process signals. You do this by using the kill command (do man kill) with the relevant signal number and process ID. For convenience you probably want to issue the kill command from a different shell window (use ps u to get the ID) to the one that you run the executable in.
  • When the signal handler completes, where does the code continue executing?
Frequently students submit programs that compile fine, but fail to terminate properly and end up running forever. Not surprisingly this causes problems when trying to automate the running and testing of their programs. To emulate this behavior the following code uses a random number generator that causes the code to go into an infinite loop ~20% of the times it is executed.
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/time.h>
    int main(int argc, char* argv[]) {
      double value;
      long int iseed;
      struct timeval tv;
    
      /* Use time of day to generate seed for random number generator*/
      gettimeofday(&tv, NULL);
      iseed = tv.tv_usec;
      srand48(iseed);
      value  = drand48();
    
      if (value < 0.8) {
        /* 80%  of time code terminates normally */
        printf("Process %d will terminate \n", getpid());
        sleep(1);
      } else {
        /* 20%  of time code runs for ever */
        printf("Process %d will run for ever \n", getpid());
        while (1) 
          sleep(1);
      }
      return 0;
    }
    
ACTIONS
  • Compile and run the above code several times. Verify that it does sometimes run forever.

  • Now using what you learned in Lab 7 and your knowledge of signals, write another program that will allow this program to run for 5 seconds, then if it has not finished normally send it a SIGKILL. Do this by having your program fork one child process. Have the child process execute the student program by issuing an exec() system call. Have the parent task sleep for 5 seconds after which time it kills the child process (which is now running the student code) by sending it a SIGKILL signal. To account for cases when the child finishes normally, include in your code an signal handler that captures the SIGCHLD signal (the signal issued when a child process completes). In the event that the parent needs to kill the child process (see man 2 kill), it should print out the message Bad student - must be a software engineer!, while in the event that the child completes normally, it should print out the message Good student - must be a scientist!. After either of these messages are printed, the parent should terminate. Note that in the event that you need to kill the child process, you should issue a sigaction() system call to set the action for the SIGCHLD signal to SIG_IGN (fail to do this and you may end up getting both the good; and bad messages printed).
  • Assuming the above executable is called a.out, trace the system calls using strace -f a.out. Try to indeitify the system calls corresponding to process creation and signals. Also do man strace to read more about system call tracing. In general, this is a powerful tool in systems programming, and can be useful for debugging.

Unix Semaphores


Just to fully convince you that introducing concurrent processing with Ada95 is much easier than using Unix we will now look briefly at using Unix semaphores (or maybe we will look at Unix semaphores because they are important in their own right, especially since they can be shared by completely separate programs).

To obtain a Unix semaphore we use the semget() function:

       int semget(key_t key, int nsems, int semflg);
This returns an identifier that is used to manipulate the semaphore, however rather than obtaining just a single semaphore this function allows the user to allocate an array of nsems semaphores. For this reason, the return value is referred to as the semaphore set id, and therefore to identify a specific semaphore we need to specify both the semaphore set id AND the number of the semaphore within this semaphore set. (You should also be aware that there are system dependent limits on the number of semaphores in a semaphore array. E.g. on Linux look in /proc/sys/kernel/sem or /usr/include/linux/sem.h. On Solaris look in /etc/system. You should also consider why such semaphores areallocated in sets, rather than individually)

Of the other parameters of this system call, the first is a key that we will brush over for the moment, while the third is used to determine whether, for example, we want to create a new semaphore set and if so what access permissions to assign it.

Having created a semaphore set it can be manipulated using calls to semctl():

       int semctl(int semid, int semnum, int cmd, ...);
We will use this function to initialize the value of each semaphore. (do man semctl; for more info). While to do the traditional "P" and "V" semaphore operations we use:
       int semop(int semid, struct sembuf *sops, unsigned nsops);
...pretty easy really!!

The following code puts this all together. First it creates a semaphore set with 2 elements. It then initializes the first semaphore within this set a value of 1 while gives the second a value of 0. It then does a double check to verify that these values are set, before forking a second process. Since the semaphore array is created before the call to fork both the parent and the child have access to it. Each process then enters a loop that selectively decrements or increments the first semaphore (ignoring the second semaphore for the time being).

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <signal.h>
    #include <sys/wait.h>
    #include <sys/sem.h>
    #include <sys/types.h>
    #include <sys/ipc.h>
    
    union semun {
      int val;
      struct semid_ds *buf;
      ushort *array;
    };
    int main(int argc, char* argv[]) {
      key_t sem_key; int nsems, semflg;     // used to set up semaphore array
      int semid;                            // OS-assigned id of semaphore array
      union semun semctl_arg;               // used to set elements of array 
      struct sembuf sem0_P, sem0_V, sem1_P, sem1_V;// specify P & V ops for each
      int irpt; pid_t fork_pid;
    
      /* SEMAPHORE CREATION */
      /* create a new semaphore with PRIVATE key */
      sem_key = IPC_PRIVATE;
      /* number of semaphores in the array */
      nsems = 2;
      /* set flag to creation with user read/write access permissions */
      semflg = IPC_CREAT | 0600;
      /* Now create the semaphore array */
      semid = semget(sem_key, nsems, semflg);
      if (semid < 0) {
        perror("Semaphore creation error"); 
        exit(-1);
      }
      printf("Semaphore created - ID = %d\n", semid);
    
      /* SEMAPHORE INITIALISATION */
      /* initialize element 0 of the semaphore array to 1 */
      semctl_arg.val = 1;
      if (semctl(semid, 0, SETVAL, semctl_arg) < 0) {
        perror("Semaphore 0 initialisation error ");
        exit(-1);
      }
      /* initialize element 1 of the semaphore array to 0 */
      semctl_arg.val = 0;
      if (semctl(semid, 1, SETVAL, semctl_arg) < 0) {
        perror("Semaphore 1 initialisation error ");
        exit(-1);
      }
    
      /*CHECK VALUE OF SEMAPHORES */
      printf("Value of semaphore array 0 %d\n", semctl(semid, 0, GETVAL));
      printf("Value of semaphore array 1 %d\n", semctl(semid, 1, GETVAL));
    
      /* setup semaphore op buffers for P (decrement) and V (increment) */
      sem0_P.sem_num = 0; sem0_P.sem_op = -1; sem0_P.sem_flg = 0;
      sem0_V.sem_num = 0; sem0_V.sem_op = +1; sem0_V.sem_flg = 0;
       
      /*FORK A SECOND PROCESS - THEY WILL SHARE THE SEMAPHORE ARRAY */
      fork_pid = fork(); 
      
      /* loop 10 times and manipulate the semaphore */
      for (irpt=0; irpt<10; irpt++) {
        /* SEMAPHORE GIVES MUTUAL EXCLUSION OF PARENT/CHILD IN FOLLOWING REGION*/
    
        /* Semaphore op - waits until semaphore > 0 then proceed */
        if (semop(semid, &sem0_P, 1)) {
          perror("semaphore decrement error");
          exit(-1);
        }
        printf("%s %d\n", fork_pid? "Parent": "Child", irpt);
        fflush(stdout);
        /* Semaphore op - increment semaphore - no waiting */
        if (semop(semid, &sem0_V, 1)) {
          perror("semaphore increment error");
          exit(-1);
        }
        /* END MUTUAL EXCLUSION */
    
      } // for(...)
    
      /* Have parent wait for child to terminate for tidy output! */
      if (fork_pid)
         wait(NULL);
      return 0;
    }
    
ACTIONS
  • Compile and run the above code ONCE.
  • Now issue the shell command ipcs -a you should see something like:
      ------ Shared Memory Segments --------
      key        shmid      owner      perms      bytes      nattch     status      
      
      ------ Semaphore Arrays --------
      key        semid      owner      perms      nsems     
      0x002fa327 0          root      666        2         
      0x00000000 294914     u8914893  600        2         
      
      ------ Message Queues --------
      key        msqid      owner      perms      used-bytes   messages    
      
    Your first lesson is that Unix semaphore arrays (and shared memory segments and message queues) are persistent. That is they are NOT removed when your program terminates or indeed even when you log out! Why? (This should be obvious.)

  • Now run the program again and re-inspect to see how many semaphore arrays you have - there should be two! This is because if we use key = IPC_PRIVATE **and** the semflg parameter includes IPC_CREAT, then every time we run the code we get a new semaphore array. If we change from using IPC_PRIVATE to giving a specific key (e.g. key = (key_t) 0xabcd) then semget() will create a new semaphore if no semaphore with that key exists, but if one does exists it will use the existing one (if one exists with that key but has the wrong array size the function call returns an error). Thus by knowing the value of a semaphore key means that two or more processes can share the same semaphore array.

  • You should be aware that on any system there is a maximum total number of semaphore arrays, shared memory segments and message queues that are allowed to exist at any one time. Typically this number is not particularly large. Thus as a good citizen of a shared computing resource you should always remove your unwanted semaphores, shared memory segments and message queues (or IPC constructs). You can do this from your code, or from your login shell by using the ipcrm command (do man ipcrm). We will use ipcrm from your login shell, but since you may have many unwanted semaphore arrays you may find the following script to remove them all useful:
      #!/bin/csh -f
      set us="$user"
      foreach sh (` ipcs -s | grep $us | awk '{print $2}' `)
        ipcrm -s $sh
      end
      
    Save this code to a file, make sure it has execute permissions, and then execute this file.

  • Returning to the semaphore code, when you ran this you probably found that it printed out something like:
      Semaphore created - ID = 393219
      Value of semaphore array 0 1
      Value of semaphore array 1 99
      Child  0
      Child  1
      Child  2
      Child  3
      Child  4
      Child  5
      Child  6
      Child  7
      Child  8
      Child  9
      Parent 0
      Parent 1
      Parent 2
      Parent 3
      Parent 4
      Parent 5
      Parent 6
      Parent 7
      Parent 8
      Parent 9
      
    This is fine, but the intention was actually to use the semaphore to cause the Parent/Child to execute the region of mutual exclusion in an alternating fashion. So that we get output that looks like:
      Semaphore created - ID = 98307
      Value of semaphore array 0 1
      Value of semaphore array 1 99
      Parent 0
      Child  0
      Parent 1
      Child  1
      Parent 2
      Child  2
      Parent 3
      Child  3
      Parent 4
      Child  4
      
    Your task is to modify the above code to achieve this. Do this by using both of the elements of the semaphore array. To ensure that you do this correctly augment the printout to include the current wall-clock time on the parent or child process (use gettimeofday() to obtain the wall-clock time). Hint: the operative word is alternating; another approach is to recognize that a semaphore can be used to implement a shared variable.

Next Lab

Shared memory segments and requeue implementations ...... just joking!

If you do want to learn about shared memory, read the man pages for shmget, shmat, shmctl and shmdt, and/or take COMP4300 in the future. If you want to do more Ada take COMP4330, while if you want to learn more computer architecture, build a cluster and run some applications on it do COMP3320.

PS. it is not just software engineers who write code that runs forever - just mainly software engineers!