Semaphores are a critical aspect of inter-process communication (IPC) in Linux. They are a way for multiple processes to synchronize their actions and coordinate their access to shared resources. In this article, we’ll explore what semaphores are, how they work, and how to implement them in Linux.
What are Semaphores?
Semaphores were first introduced by Dutch computer scientist Edsger W. Dijkstra in 1965 as a means of controlling access to shared resources in concurrent programming. The concept was later incorporated into the Unix operating system and, eventually, into the Linux kernel.
A semaphore is a variable used to control access to a shared resource by multiple processes. It’s a simple yet powerful mechanism that allows processes to coordinate their access to shared resources and avoid race conditions. A semaphore is typically implemented as an integer variable with two basic operations:
- increment (post or signal)
- decrement (wait or acquire)
The increment operation (post or signal) increases the value of the semaphore, while the decrement operation (wait or acquire) decreases the value of the semaphore. When the value of the semaphore is positive, it indicates that the resource is available, and processes can access it. When the value of the semaphore is zero, it indicates that the resource is not available, and processes must wait until the semaphore is incremented.
Types of Semaphores
There are two types of semaphores: binary semaphores and counting semaphores.
Binary semaphores are semaphores that can have only two values: 0 and 1. They are used to synchronize access to a single shared resource. When a binary semaphore’s value is 1, it indicates that the resource is available, and when its value is 0, it indicates that the resource is not available.
Counting semaphores, on the other hand, can have a value greater than 1. They are used to synchronize access to multiple shared resources. The value of a counting semaphore indicates the number of available resources.
Implementing Semaphores in Linux
In Linux, semaphores can be implemented using the System V IPC (Inter-Process Communication) facilities. The System V IPC facilities include semaphores, shared memory, and message queues. In this article, we’ll focus on semaphores and how to implement them in Linux.
The semget() system call is used to create a new semaphore or access an existing semaphore. The first argument to semget() is the key, which is used to identify the semaphore. The second argument is the number of semaphores to create, and the third argument is the semaphore flags.
The semop() system call is used to perform semaphore operations. The first argument to semop() is the semaphore identifier, which is returned by semget(). The second argument is a pointer to a sembuf structure, which specifies the operations to be performed on the semaphore.
The semctl() system call is used to control the semaphore. The first argument to semctl() is the semaphore identifier, which is returned by semget(). The second argument is the semaphore number, and the third argument is the command to be performed.
Example of Semaphore Implementation in Linux
Here’s a simple example of a semaphore implementation in Linux using the System V IPC facilities:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#define SEM_KEY 0x1234
#define BUFFER_SIZE 10
int buffer[BUFFER_SIZE];
int in = 0;
int out = 0;
int count = 0;
void produce(int semid)
{
struct sembuf sem_op;
sem_op.sem_num = 0;
sem_op.sem_op = -1;
sem_op.sem_flg = 0;
semop(semid, &sem_op, 1);
buffer[in] = count++;
in = (in + 1) % BUFFER_SIZE;
sem_op.sem_num = 0;
sem_op.sem_op = 1;
semop(semid, &sem_op, 1);
}
void consume(int semid)
{
struct sembuf sem_op;
sem_op.sem_num = 0;
sem_op.sem_op = -1;
sem_op.sem_flg = 0;
semop(semid, &sem_op, 1);
int item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
sem_op.sem_num = 0;
sem_op.sem_op = 1;
semop(semid, &sem_op, 1);
printf("Consumed item: %d\n", item);
}
int main()
{
int semid = semget(SEM_KEY, 1, IPC_CREAT | 0666);
if (semid < 0)
{
perror("semget");
return 1;
}
if (semctl(semid, 0, SETVAL, 1) < 0)
{
perror("semctl");
return 1;
}
if (fork() == 0)
{
for (int i = 0; i < 5; i++)
{
produce(semid);
}
}
else
{
for (int i = 0; i < 5; i++)
{
consume(semid);
}
}
return 0;
}
In this example, the semaphore is used to synchronize access to the shared buffer. The producer and consumer processes use the semaphore to control access to the buffer. The semaphore is created with semget() and initialized with a value of 1 using semctl(). The semaphore operations (wait and signal) are performed using semop(). The producer process increments the semaphore and the consumer process decrements it to synchronize access to the buffer.