System V Semaphores

Difference between revision 27 and current revision

No diff available.

Controlling resources with semaphores

Problem: how can I control access to resources?

Imagine a printer spooler with five printers attached. The resources (the printers) are represented by a special integer (a semaphore) which ranges from 0 to 5. 5 means there are 5 printers free, 0 means all printers are busy. When a job arrives at the printerspooler, the spooler checks the semaphore if a printer is free or not (i.e. if it is >0). If so, it decrements the semaphore and relays the printerjob to a printer. When the printer signals that it has finished a job, the spooler increments the semaphore.

Now imagine a struct shared_var which must be accessible by two processes. Then it could be possible that both would modify the variable at the same time. This is undesirable, because this could result in all sorts of unpredictable behaviour.

Here too, semaphores help to solve this problem. In the case of shared_var, the maximum is one. This means, there is only one place for a process. To access shared_var, a process checks its semaphore shared_var_sem to see if it is one. Then the process sets the semaphore to zero and changes shared_var. After completion, shared_var_sem is raised by one to indicate other processes are free to change shared_var. This special type of semaphore is a binary semaphore. The situation which occurs here is mutual exclusion, since neither process can interrupt the other one.

There is only one problem: what if the OS process scheduler decides to run another process while a semaphore is being changed? The answer: this is not possible. The instructions are implemented in an atomic way, that is, they can not be interrupted - guaranteed. You have probably seen the same guarantee is given for signals.

One semaphore for each resource

Note though, that you need one semaphore for each resource (or "action", or "piece of code"). The initial value of the semaphore is essentially the number of processes that can do something to the resource at the same time.

Example: suppose you have one producer process which "produces" data by reading from a hardware device. And suppose you have one consumer process which needs to read this data. They are separate processes but the producing/consuming has to work in turn. We can't have the producer overwriting previous data, or the consumer do multiple readings of the same data.

This calls for two semaphores. Namely: one semaphore which guards the producing resource (let's call it PROD_SEM) and one which guards the consuming resource (CONS_SEM). Think about this for a bit.

Initially, PROD_SEM will be set to 1, indicating there is 1 place in the queue for a producer to do his job. The CONS_SEM will be set to 0, indicating there is currently no place for something to consume.

What semaphores don't do

All a semaphore does, is limit the number of concurrent accesses to a certain place in your code. That's it -- it's a really low level mechanism.

Now, you can set this number initially to 1, or make it as high as you want. However, the value of the semaphore is not really important for the piece of code that's trying to get access. You shouldn't use it as a way of prioritizing or place in some sort of queue.

Mostly, you'll use a semaphore as a binary semaphore (a mutex), to guard a piece of code that should only be run once at a time. This is where you'll find that System V semaphores can be overkill. They can deal with sets of semaphores and arrays of operations on semaphores, while most of the time, you just need a mutex. The pthread library offers much simpler functionality for this.

The API

Creating and opening semaphore sets with semget()

As can be seen by the heading of this section, semaphores are implemented in sets. Such a set can contain up to 512 semaphores. Creating or opening a semaphore set is done by the same function:

 int semget ( key_t key, int nsems, int semflg );

An explanation of the parameters:

 key_t key

The key, a value to identify the semaphore set system-wide. Best practice is to use the ftok() function to convert your binary path plus name to a key, though you can set any key. Can also be IPC_PRIVATE, which will strip the third parameter int semflg, so only the permission part is left.

 int nsems

The number of semaphores which you want your new semaphore set to have. Ignored when semget() is only opening a semaphore set, not creating one. If you're opening a semaphore set and you're passing the wrong number of semaphores here, you'll get an EINVAL.

 int semflg

This parameter is divided in parts, which must be glued together with the bitwise OR operator (|). One part can be IPC_CREATE. Or IPC_CREATE|IPC_EXCL. The other part is the permission part. It consists of nine bits which represent the wellknown permission bits rwxrwxrwx. For more information, check the examples.

On success, semget() will return an integer. This integer is needed when manipulating the set with semop() and semctl().

Examples

In the examples below, it is assumed that the following macro and declarations have been made. Here we use DONTCARE as a parameter value to explicitly tell you that you don't need to delve deeper into the meaning of this parameter. You'll probably use NULL instead.

 #define DONTCARE 0             /* to indicate that this parameter is ignored */
 key_t n_sem_key = 1234;                     /* the key for our semaphore set */
 int n_sem_id;                            /* unique value for a semaphore set */
 int n_no_sems = 5;                    /* the number of semaphores in our set */

Create a semaphore set with five semaphores. The 3rd and last parameter gives all rights to the creator (the first six). It gives read rights to the group of the creator and others (the second and third six). Remember that the prefixed 0 (zero) tells your compiler that the number is octal.

 sem_id =  semget(IPC_PRIVATE, n_no_sems, 0644);

Create a semaphore set. If creating doesn't succeed, because a set already exists with the same key, -1 is returned and errno is set to EEXIST.

 sem_id = semget(sem_key, n_no_sems, IPC_CREAT | IPC_EXCL | 0644);

Open a semaphore set. If it does not exist, -1 is returned and errno is set to ENOENT.

 sem_id = semget(sem_key, DONTCARE, DONTCARE);

Open the semaphore set with key sem_key. If opening doesn't succeed, because the set does not exist, try to create it.

 sem_id = semget(sem_key, n_no_sems, IPC_CREAT | 0644);

Manipulating semaphore sets with semop()

To do an operation on a semaphore set (increment or decrement a semaphore) we use semop().

 int semop ( int semid, struct sembuf *sops, unsigned nsops );

An explanation of the parameters:

 int semid

The unique identifier of the semaphore set on which the operation(s) have to take place.

 struct sembuf *sops

Here you pass the address of an array of (predefined) struct sembuf. Each struct sembuf describes an operation.

 unsigned nsops

The number of operations in the array of the previous parameter.

As said, each struct sembuf describes one piece of work which must be carried out by semop(). So the procedure is:

  1. Fill one, or a whole array of sembuf structs
  2. Call semop()
  3. Then semop() will perform the operations as defined by the structs.

Note that the array of sembuf structs can be as small as one.

The struct sembuf is declared in sem.h as follows:

 struct sembuf {
         unsighed short integer  sem_num;
         short integer           sem_op;
         short integer           sem_flg;
 }

Explanation of the parameters:

 unsighed short integer sem_num;

This is the semaphore on which you want to perform the operation.

 short integer sem_op;
 short integer sem_flg;

If you pass 0, then the process will block until this operation is performed. If you pass IPC_NOWAIT, the call will either succeed immediately, or fail and continue. If you pass SEM_UNDO, then the operation will be undone when the process exits. But check your man pages for additional notes on this one!

In the examples below, it is assumed that semaphore set sem_id is open. Also, the following macro and declarations have been made:

 #define DONTCARE 0             /* to indicate that this parameter is ignored */
 int c_sem_in_set;           /* a counter for loops which fill sembuf structs */
 int n_retval_semop;                     /* the value which semop will return */
 int n_no_sems = 5;                    /* the number of semaphores in our set */
 struct sembuf sem_oper;         /* describes an operation on a semaphore set */
 struct sembuf sem_opers[n_no_sems]; /* several operations on a semaphore set */

Block process until semaphore 3 can be lowered by one.

 sem_oper = {3, -1, 0);
 n_retval_semop = semop(sem_id, &sem_oper, 1);

Try to lower semaphore 3 by one. If it does not succeed, continue.

 sem_oper = {3, -1, IPC_NOWAIT);
 n_retval_semop = semop(sem_id, &sem_oper, 1);
 if(n_retval_semop == -1)
 {
       if(errno == EAGAIN)
       {
             /* do whatever you have to do now that the operation could not
              * go through without blocking */
       }
       else
       {
             /* do some error handling here, because an error has occurred 
              * and it is NOT because the operation could not go through
              * without blocking */
       }
 }

Block process until all semaphores are lowered by one.

 for (c_sem_in_set = 0; c_sem_in_set < n_no_sems; c_sem_in_set++)
 {
       sem_opers[c_sem_in_set] = {c_sem_in_set, -1, 0);
 }
 n_retval_semop = semop(sem_id, &sem_opers, 1);

Controlling semaphore sets with semctl()

To perform control operations on a single semaphore or on a complete set, we use semctl(). Control operations are at a higher level than normal operations: GETALL catches all values in one set, GETPID gives the PID of the process who last did an operation on a certain semaphore, etc. Important is: the control operations are atomic, so in one atomic action one can, for instance, reset a whole semaphore set.

This system call has four arguments and the last two are the most important: the 3th argument specifies the actual control operation and the 4th argument forms the characteristics of the control operation.

 int semctl ( int semid, int semnum, int cmd, union semun arg );
    
 int semid

The unique identifier of the semaphore set on which the control operation(s) have to take place.

 int semnum

The number of the semaphore at which the control operation is targeted. This number can be thought of as an index of the semaphore set, the first semaphore in the semaphore set having the number 0.

 int cmd

There are many commands which can be performed. See your man pages.

 union semun arg

The arg argument represents an instance of type semun. This particular union is declared as follows:

    /* argument for semctl() system calls. */
    union semun {
            int val;                /* value for SETVAL */
            struct semid_ds *buf;   /* buffer for IPC_STAT & IPC_SET */
            ushort *array;          /* array for GETALL & SETALL */
    };             
 int val

Used when the SETVAL command is performed. SETVAL, of course, sets the values of all semaphores.

 struct semid_ds *buf

Used in the IPC_STAT and IPC_SET commands. Represents a copy of the internal semaphore data structure used in the kernel.

 ushort *array

A pointer used in the GETALL and SETALL commands. Should point to an array of integer values to be used in setting or retrieving all semaphore values in a set.

In the example below, it is assumed that semaphore set sem_id is open. Also, the following macro and declarations have been made:

 #define SEM_1    0             /* Name for first semaphore in the set */
 union semun semctl_arg;        /* the argument for our calls to semctl() */

Change all semaphores in semaphore set sem_id to 5.

 semctl_arg.val = 5;        /* the value which will be saved in the whole set */
 semctl(sem_id, SEM_1, SETALL, semctl_arg);

Remove the whole set. Any processes which were blocking, get an error return.

 semctl(sem_id, SEM_1, IPC_RMID, DONTCARE);

Sometimes you want to see the current value of a semaphore in a set:

 int retval = semctl(sem_id, SEM_1, GETVAL, NULL);
 printf("sem_id %d initialized to %d\n", sem_id, retval);

Full example

The following program will initiate a semaphore array with one semaphore, set it to 1 and then try to lower it.

 #include <sys/ipc.h>
 #include <sys/shm.h>
 #include <sys/sem.h>
 #include <sys/types.h>
 #include <errno.h>
 #include <string.h>
 #include <stdio.h>
 #include <stdlib.h>
 
 /* Must define this ourselves according to "man semctl" */
 union semun {
     int val;                  /* value for SETVAL */
     struct semid_ds *buf;     /* buffer for IPC_STAT, IPC_SET */
     unsigned short *array;    /* array for GETALL, SETALL */
                               /* Linux specific part: */
     struct seminfo *__buf;    /* buffer for IPC_INFO */
 };
 
 /* Semaphore necessary for proper locking of shared memory */
 #define N_SEMS         1       /* Number of semaphores in semaphore array */
 #define ACCESS         0       /* To guard whether we're writing or not */
 #define DOWN           -1      /* Operation on a semaphore */
 #define UP             1       /* Operation on a semaphore */
 
 int init()
 {
     key_t key = 1234;     /* the key for our shared memory object */
     int retval;
     int sem_id;
     union semun sem_cmd;
 
     /* First get the ID of the semaphore, creating if it didn't exist yet */
     /* Open or create the semaphore array with one entry */
     sem_id = semget(key, N_SEMS, IPC_CREAT | 0644);
     if(sem_id < 0) {
         perror("Error creating semaphore set");
         return -1;
     }
 
     /* Initialize semaphore */
     sem_cmd.val = 0;
     retval = semctl(sem_id, ACCESS, SETVAL, sem_cmd);
     if(retval < 0) {
         perror("Error initializing semaphore ACCESS");
         return -1;
     }
     return sem_id;
 }
 
 int main(void)
 {
     int sem_id;
     int retval;
     struct sembuf down = {ACCESS, DOWN, IPC_NOWAIT}; 
     struct sembuf up = {ACCESS, UP, IPC_NOWAIT};    
 
     sem_id = init();
     if(sem_id < 0) {
         exit(EXIT_FAILURE);
     }
 
     /* Semaphore initialized at 1, now lower it */
     retval = semop(sem_id, &down, N_SEMS);
     if(retval < 0) {
         if(errno == EAGAIN) {
             printf("Skipping lowering the semaphore\n");
             return EXIT_SUCCESS;
         }
         printf("Error lowering the semaphore\n");
         exit(EXIT_FAILURE);
     }
 
     retval = semctl(sem_id, IPC_RMID, 0);
     if(retval == -1)
     {
         printf("Couldn't clean up semaphore\n");
         exit(EXIT_FAILURE);
     }
 
     printf("Everything OK\n");
 
     return EXIT_SUCCESS;
 }

Timing out

Below is an example where semop() times out, which is very useful. Deadlocks can be detected with a nice error message. The code below will always time out. We initialize the semaphore with value 0 and then immediately try to lower it.

 #include <stdio.h>
 #include <sys/types.h>
 #include <sys/ipc.h>
 #include <sys/sem.h>
 
 /* Must define this according to "man semctl" */
 union semun {
     int val;                    /* value for SETVAL */
     struct semid_ds *buf;       /* buffer for IPC_STAT, IPC_SET */
     unsigned short *array;      /* array for GETALL, SETALL */
                                 /* Linux specific part: */
     struct seminfo *__buf;      /* buffer for IPC_INFO */
 };
 
 
 int main(void)
 {
     struct sembuf down = {0, -1, 0};
     int sem_id;
     int retval;
     union semun sem_cmd;
     struct timespec timeout = {10, 0}; /* Wait ten seconds */
 
     /* Open or create the semaphore array with one entry */
     sem_id = semget(1234, 1, IPC_CREAT | 0666);
     if(sem_id < 0) {
         perror("semget");
         return -1;
     }
     printf("Got sem_id %d\n", sem_id);
 
     /* Initialize semaphore */
     sem_cmd.val = 0;
     retval = semctl(sem_id, 0, SETVAL, sem_cmd);
     if(retval < 0) {
         perror("semctl");
         return -1;
     }
     printf("sem_id %d initialized\n", sem_id);
 
     retval = semtimedop(sem_id, &down, 1, &timeout);
     /* First get the ID of the semaphore, creating if it didn't exist yet */
     if(retval < 0) {
         if(retval == -EAGAIN) {
             printf("Time out!\n");
         } else {
             perror("semtimedop");
         }
         return -1;
     }
     printf("sem_id %d downed\n", sem_id);
 
     return 0;
 }

Tips

Since using semaphores can be tricky the first few times, a good advice is to log current values before and after doing an operation on a semaphore. The following piece of code is part of a larger body that guards access to a serial port. Upon disconnecting to a server, the following function is called.

Note that this function won't compile of itself, other necessary functions aren't provided -- this code is meant as an example only.

 /* Unlocks serial port.
  * P1: String which is passed with logged messages
  */
 int unlock_serial(char* log_id)
 {
     struct sembuf up = {ACCESS_COM, UP, 0}; /* Operation on semaphore */
     int sem_id = get_sem_id();
     int retval;
 
     /* Now try to lower the access comport semaphore */
     retval = semctl(sem_id, ACCESS_COM, GETVAL);
     print_info(log_id, "ACCESS_COM semaphore %d value: %d\n", sem_id,
         retval);
     print_info(log_id, "Raising ACCESS_COM semaphore\n");
     retval = semop(sem_id, &up, 1);
     if(retval < 0) {
         print_err(log_id, "Error raising the ACCESS_COM semaphore: %s\n",
             strerror(errno));
         return -1;
     }
     retval = semctl(sem_id, ACCESS_COM, GETVAL);
     print_info(log_id, "Raised ACCESS_COM semaphore %d to %d\n", sem_id,
         retval);
     return 0;
 }

Exercises

Semaphores might take some getting used to. Some good starting exercises could be:

Resources