Communication inter-processus - Sémaphores
La première question qui me vient à l'esprit est: pourquoi avons-nous besoin de sémaphores? Une réponse simple, pour protéger la région critique / commune partagée entre plusieurs processus.
Supposons que plusieurs processus utilisent la même région de code et si tous veulent accéder en parallèle, le résultat se chevauche. Supposons, par exemple, que plusieurs utilisateurs n'utilisent qu'une seule imprimante (section commune / critique), disons 3 utilisateurs, pour 3 tâches en même temps, si toutes les tâches démarrent en parallèle, une sortie utilisateur se chevauche avec une autre. Nous devons donc protéger cela en utilisant des sémaphores, c'est-à-dire en verrouillant la section critique lorsqu'un processus est en cours d'exécution et en le déverrouillant une fois terminé. Cette opération serait répétée pour chaque utilisateur / processus afin qu'un travail ne se chevauche pas avec un autre travail.
Fondamentalement, les sémaphores sont classés en deux types -
Binary Semaphores - Seulement deux états 0 et 1, c'est à dire, verrouillé / déverrouillé ou disponible / indisponible, implémentation Mutex.
Counting Semaphores - Les sémaphores qui permettent un comptage arbitraire de ressources sont appelés sémaphores de comptage.
Supposons que nous ayons 5 imprimantes (pour comprendre, supposons qu'une imprimante n'accepte qu'un seul travail) et que nous ayons 3 travaux à imprimer. Maintenant, 3 emplois seraient attribués pour 3 imprimantes (1 chacune). Encore une fois, 4 emplois sont venus pendant que cela est en cours. Maintenant, sur 2 imprimantes disponibles, 2 travaux ont été planifiés et il nous reste 2 travaux supplémentaires, qui ne seraient terminés qu'après la disponibilité de l'une des ressources / imprimante. Ce type de planification selon la disponibilité des ressources peut être considéré comme comptant des sémaphores.
Pour effectuer la synchronisation à l'aide de sémaphores, voici les étapes:
Step 1 - Créer un sémaphore ou se connecter à un sémaphore déjà existant (semget ())
Step 2 - Effectuer des opérations sur le sémaphore ie, allouer ou libérer ou attendre les ressources (semop ())
Step 3 - Effectuer des opérations de contrôle sur la file d'attente des messages (semctl ())
Maintenant, vérifions cela avec les appels système que nous avons.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg)
Cet appel système crée ou alloue un ensemble de sémaphores System V. Les arguments suivants doivent être passés -
Le premier argument, clé, reconnaît la file d'attente des messages. La clé peut être une valeur arbitraire ou une valeur qui peut être dérivée de la fonction de bibliothèque ftok ().
Le deuxième argument, nsems, spécifie le nombre de sémaphores. Si binaire, alors il vaut 1, implique le besoin d'un ensemble de sémaphores, sinon selon le nombre requis d'ensembles de sémaphores.
Le troisième argument, semflg, spécifie le ou les indicateurs de sémaphore requis tels que IPC_CREAT (création d'un sémaphore s'il n'existe pas) ou IPC_EXCL (utilisé avec IPC_CREAT pour créer un sémaphore et l'appel échoue, si un sémaphore existe déjà). Vous devez également passer les autorisations.
Note - Reportez-vous aux sections précédentes pour plus de détails sur les autorisations.
Cet appel renverrait un identifiant de sémaphore valide (utilisé pour d'autres appels de sémaphores) en cas de succès et -1 en cas d'échec. Pour connaître la cause de l'échec, vérifiez avec la variable errno ou la fonction perror ().
Diverses erreurs concernant cet appel sont EACCESS (autorisation refusée), EEXIST (la file d'attente existe déjà ne peut pas être créée), ENOENT (la file d'attente n'existe pas), ENOMEM (pas assez de mémoire pour créer la file d'attente), ENOSPC (limite d'ensembles maximum dépassé), etc.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *semops, size_t nsemops)
Cet appel système exécute les opérations sur les ensembles de sémaphores System V, à savoir l'allocation des ressources, l'attente des ressources ou la libération des ressources. Les arguments suivants doivent être passés -
Le premier argument, semid, indique l'identifiant de l'ensemble de sémaphores créé par semget ().
Le deuxième argument, semops, est le pointeur vers un tableau d'opérations à effectuer sur l'ensemble de sémaphores. La structure est la suivante -
struct sembuf {
unsigned short sem_num; /* Semaphore set num */
short sem_op; /* Semaphore operation */
short sem_flg; /* Operation flags, IPC_NOWAIT, SEM_UNDO */
};
L'élément, sem_op, dans la structure ci-dessus, indique l'opération à effectuer -
Si sem_op vaut –ve, allouez ou obtenez des ressources. Bloque le processus appelant jusqu'à ce que suffisamment de ressources aient été libérées par d'autres processus, afin que ce processus puisse allouer.
Si sem_op vaut zéro, le processus appelant attend ou dort jusqu'à ce que la valeur du sémaphore atteigne 0.
Si sem_op vaut + ve, libérez les ressources.
Par exemple -
struct sembuf sem_lock = {0, -1, SEM_UNDO};
struct sembuf sem_unlock = {0, 1, SEM_UNDO};
Le troisième argument, nsemops, est le nombre d'opérations dans ce tableau.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, …)
Cet appel système effectue une opération de contrôle pour un sémaphore System V. Les arguments suivants doivent être passés -
Le premier argument, semid, est l'identifiant du sémaphore. Cet identifiant est l'identifiant du sémaphore, qui est la valeur de retour de l'appel système semget ().
Le deuxième argument, semnum, est le nombre de sémaphore. Les sémaphores sont numérotés à partir de 0.
Le troisième argument, cmd, est la commande pour effectuer l'opération de contrôle requise sur le sémaphore.
Le quatrième argument, de type union semun, dépend de la cmd. Dans quelques cas, le quatrième argument n'est pas applicable.
Vérifions l'union semun -
union semun {
int val; /* val for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT and IPC_SET */
unsigned short *array; /* Buffer for GETALL and SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO and SEM_INFO*/
};
La structure de données semid_ds qui est définie dans sys / sem.h est la suivante -
struct semid_ds {
struct ipc_perm sem_perm; /* Permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* Number of semaphores in the set */
};
Note - Veuillez consulter les pages de manuel pour les autres structures de données.
union semun arg; Les valeurs valides pour cmd sont -
IPC_STAT- Copie les informations des valeurs actuelles de chaque membre de la structure semid_ds dans la structure passée pointée par arg.buf. Cette commande nécessite une autorisation de lecture sur le sémaphore.
IPC_SET - Définit l'ID utilisateur, l'ID de groupe du propriétaire, les autorisations, etc. pointés par la structure semid_ds.
IPC_RMID - Supprime l'ensemble de sémaphores.
IPC_INFO - Renvoie les informations sur les limites et les paramètres du sémaphore dans la structure semid_ds pointée par arg .__ buf.
SEM_INFO - Renvoie une structure séminfo contenant des informations sur les ressources système consommées par le sémaphore.
Cet appel renverrait une valeur (valeur non négative) en fonction de la commande passée. En cas de succès, IPC_INFO et SEM_INFO ou SEM_STAT retourne l'index ou l'identificateur de l'entrée la plus utilisée selon Semaphore ou la valeur de semncnt pour GETNCNT ou la valeur de sempid pour GETPID ou la valeur de semval pour GETVAL 0 pour les autres opérations en cas de succès et - 1 en cas de panne. Pour connaître la cause de l'échec, vérifiez avec la variable errno ou la fonction perror ().
Avant de regarder le code, comprenons sa mise en œuvre -
Créez deux processus, par exemple, enfant et parent.
Créez la mémoire partagée principalement nécessaire pour stocker le compteur et d'autres indicateurs pour indiquer la fin du processus de lecture / écriture dans la mémoire partagée.
Le compteur est incrémenté de nombre par les processus parents et enfants. Le nombre est soit passé comme argument de ligne de commande, soit pris comme valeur par défaut (s'il n'est pas passé comme argument de ligne de commande ou si la valeur est inférieure à 10000). Appelé avec un certain temps de sommeil pour s'assurer que le parent et l'enfant accèdent à la mémoire partagée en même temps, c'est-à-dire en parallèle.
Étant donné que le compteur est incrémenté par pas de 1 par le parent et l'enfant, la valeur finale doit être le double du compteur. Étant donné que les processus parents et enfants effectuant les opérations en même temps, le compteur n'est pas incrémenté comme requis. Par conséquent, nous devons nous assurer de l'exhaustivité d'un processus d'achèvement suivi d'un autre processus.
Toutes les implémentations ci-dessus sont effectuées dans le fichier shm_write_cntr.c
Vérifiez si la valeur du compteur est implémentée dans le fichier shm_read_cntr.c
Pour assurer la complétion, le programme sémaphore est implémenté dans le fichier shm_write_cntr_with_sem.c. Supprimez le sémaphore après la fin de l'ensemble du processus (une fois la lecture effectuée à partir d'un autre programme)
Depuis, nous avons des fichiers séparés pour lire la valeur du compteur dans la mémoire partagée et n'avons aucun effet de l'écriture, le programme de lecture reste le même (shm_read_cntr.c)
Il est toujours préférable d'exécuter le programme d'écriture dans un terminal et de lire le programme depuis un autre terminal. Étant donné que le programme n'achève l'exécution qu'une fois le processus d'écriture et de lecture terminé, il est possible d'exécuter le programme après avoir complètement exécuté le programme d'écriture. Le programme d'écriture attendra jusqu'à ce que le programme de lecture soit exécuté et ne se termine qu'une fois terminé.
Programmes sans sémaphores.
/* Filename: shm_write_cntr.c */
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#define SHM_KEY 0x12345
struct shmseg {
int cntr;
int write_complete;
int read_complete;
};
void shared_memory_cntr_increment(int pid, struct shmseg *shmp, int total_count);
int main(int argc, char *argv[]) {
int shmid;
struct shmseg *shmp;
char *bufptr;
int total_count;
int sleep_time;
pid_t pid;
if (argc != 2)
total_count = 10000;
else {
total_count = atoi(argv[1]);
if (total_count < 10000)
total_count = 10000;
}
printf("Total Count is %d\n", total_count);
shmid = shmget(SHM_KEY, sizeof(struct shmseg), 0644|IPC_CREAT);
if (shmid == -1) {
perror("Shared memory");
return 1;
}
// Attach to the segment to get a pointer to it.
shmp = shmat(shmid, NULL, 0);
if (shmp == (void *) -1) {
perror("Shared memory attach");
return 1;
}
shmp->cntr = 0;
pid = fork();
/* Parent Process - Writing Once */
if (pid > 0) {
shared_memory_cntr_increment(pid, shmp, total_count);
} else if (pid == 0) {
shared_memory_cntr_increment(pid, shmp, total_count);
return 0;
} else {
perror("Fork Failure\n");
return 1;
}
while (shmp->read_complete != 1)
sleep(1);
if (shmdt(shmp) == -1) {
perror("shmdt");
return 1;
}
if (shmctl(shmid, IPC_RMID, 0) == -1) {
perror("shmctl");
return 1;
}
printf("Writing Process: Complete\n");
return 0;
}
/* Increment the counter of shared memory by total_count in steps of 1 */
void shared_memory_cntr_increment(int pid, struct shmseg *shmp, int total_count) {
int cntr;
int numtimes;
int sleep_time;
cntr = shmp->cntr;
shmp->write_complete = 0;
if (pid == 0)
printf("SHM_WRITE: CHILD: Now writing\n");
else if (pid > 0)
printf("SHM_WRITE: PARENT: Now writing\n");
//printf("SHM_CNTR is %d\n", shmp->cntr);
/* Increment the counter in shared memory by total_count in steps of 1 */
for (numtimes = 0; numtimes < total_count; numtimes++) {
cntr += 1;
shmp->cntr = cntr;
/* Sleeping for a second for every thousand */
sleep_time = cntr % 1000;
if (sleep_time == 0)
sleep(1);
}
shmp->write_complete = 1;
if (pid == 0)
printf("SHM_WRITE: CHILD: Writing Done\n");
else if (pid > 0)
printf("SHM_WRITE: PARENT: Writing Done\n");
return;
}
Étapes de compilation et d'exécution
Total Count is 10000
SHM_WRITE: PARENT: Now writing
SHM_WRITE: CHILD: Now writing
SHM_WRITE: PARENT: Writing Done
SHM_WRITE: CHILD: Writing Done
Writing Process: Complete
Voyons maintenant le programme de lecture de la mémoire partagée.
/* Filename: shm_read_cntr.c */
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>
#define SHM_KEY 0x12345
struct shmseg {
int cntr;
int write_complete;
int read_complete;
};
int main(int argc, char *argv[]) {
int shmid, numtimes;
struct shmseg *shmp;
int total_count;
int cntr;
int sleep_time;
if (argc != 2)
total_count = 10000;
else {
total_count = atoi(argv[1]);
if (total_count < 10000)
total_count = 10000;
}
shmid = shmget(SHM_KEY, sizeof(struct shmseg), 0644|IPC_CREAT);
if (shmid == -1) {
perror("Shared memory");
return 1;
}
// Attach to the segment to get a pointer to it.
shmp = shmat(shmid, NULL, 0);
if (shmp == (void *) -1) {
perror("Shared memory attach");
return 1;
}
/* Read the shared memory cntr and print it on standard output */
while (shmp->write_complete != 1) {
if (shmp->cntr == -1) {
perror("read");
return 1;
}
sleep(3);
}
printf("Reading Process: Shared Memory: Counter is %d\n", shmp->cntr);
printf("Reading Process: Reading Done, Detaching Shared Memory\n");
shmp->read_complete = 1;
if (shmdt(shmp) == -1) {
perror("shmdt");
return 1;
}
printf("Reading Process: Complete\n");
return 0;
}
Étapes de compilation et d'exécution
Reading Process: Shared Memory: Counter is 11000
Reading Process: Reading Done, Detaching Shared Memory
Reading Process: Complete
Si vous observez la sortie ci-dessus, le compteur doit être 20000, cependant, car avant l'achèvement d'une tâche de processus, un autre processus est également traité en parallèle, la valeur du compteur n'est pas celle attendue. La sortie varierait d'un système à l'autre et elle varierait également avec chaque exécution. Pour garantir que les deux processus exécutent la tâche après l'achèvement d'une tâche, celle-ci doit être mise en œuvre à l'aide de mécanismes de synchronisation.
Maintenant, vérifions la même application en utilisant des sémaphores.
Note - Le programme de lecture reste le même.
/* Filename: shm_write_cntr_with_sem.c */
#include<stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/sem.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#define SHM_KEY 0x12345
#define SEM_KEY 0x54321
#define MAX_TRIES 20
struct shmseg {
int cntr;
int write_complete;
int read_complete;
};
void shared_memory_cntr_increment(int, struct shmseg*, int);
void remove_semaphore();
int main(int argc, char *argv[]) {
int shmid;
struct shmseg *shmp;
char *bufptr;
int total_count;
int sleep_time;
pid_t pid;
if (argc != 2)
total_count = 10000;
else {
total_count = atoi(argv[1]);
if (total_count < 10000)
total_count = 10000;
}
printf("Total Count is %d\n", total_count);
shmid = shmget(SHM_KEY, sizeof(struct shmseg), 0644|IPC_CREAT);
if (shmid == -1) {
perror("Shared memory");
return 1;
}
// Attach to the segment to get a pointer to it.
shmp = shmat(shmid, NULL, 0);
if (shmp == (void *) -1) {
perror("Shared memory attach: ");
return 1;
}
shmp->cntr = 0;
pid = fork();
/* Parent Process - Writing Once */
if (pid > 0) {
shared_memory_cntr_increment(pid, shmp, total_count);
} else if (pid == 0) {
shared_memory_cntr_increment(pid, shmp, total_count);
return 0;
} else {
perror("Fork Failure\n");
return 1;
}
while (shmp->read_complete != 1)
sleep(1);
if (shmdt(shmp) == -1) {
perror("shmdt");
return 1;
}
if (shmctl(shmid, IPC_RMID, 0) == -1) {
perror("shmctl");
return 1;
}
printf("Writing Process: Complete\n");
remove_semaphore();
return 0;
}
/* Increment the counter of shared memory by total_count in steps of 1 */
void shared_memory_cntr_increment(int pid, struct shmseg *shmp, int total_count) {
int cntr;
int numtimes;
int sleep_time;
int semid;
struct sembuf sem_buf;
struct semid_ds buf;
int tries;
int retval;
semid = semget(SEM_KEY, 1, IPC_CREAT | IPC_EXCL | 0666);
//printf("errno is %d and semid is %d\n", errno, semid);
/* Got the semaphore */
if (semid >= 0) {
printf("First Process\n");
sem_buf.sem_op = 1;
sem_buf.sem_flg = 0;
sem_buf.sem_num = 0;
retval = semop(semid, &sem_buf, 1);
if (retval == -1) {
perror("Semaphore Operation: ");
return;
}
} else if (errno == EEXIST) { // Already other process got it
int ready = 0;
printf("Second Process\n");
semid = semget(SEM_KEY, 1, 0);
if (semid < 0) {
perror("Semaphore GET: ");
return;
}
/* Waiting for the resource */
sem_buf.sem_num = 0;
sem_buf.sem_op = 0;
sem_buf.sem_flg = SEM_UNDO;
retval = semop(semid, &sem_buf, 1);
if (retval == -1) {
perror("Semaphore Locked: ");
return;
}
}
sem_buf.sem_num = 0;
sem_buf.sem_op = -1; /* Allocating the resources */
sem_buf.sem_flg = SEM_UNDO;
retval = semop(semid, &sem_buf, 1);
if (retval == -1) {
perror("Semaphore Locked: ");
return;
}
cntr = shmp->cntr;
shmp->write_complete = 0;
if (pid == 0)
printf("SHM_WRITE: CHILD: Now writing\n");
else if (pid > 0)
printf("SHM_WRITE: PARENT: Now writing\n");
//printf("SHM_CNTR is %d\n", shmp->cntr);
/* Increment the counter in shared memory by total_count in steps of 1 */
for (numtimes = 0; numtimes < total_count; numtimes++) {
cntr += 1;
shmp->cntr = cntr;
/* Sleeping for a second for every thousand */
sleep_time = cntr % 1000;
if (sleep_time == 0)
sleep(1);
}
shmp->write_complete = 1;
sem_buf.sem_op = 1; /* Releasing the resource */
retval = semop(semid, &sem_buf, 1);
if (retval == -1) {
perror("Semaphore Locked\n");
return;
}
if (pid == 0)
printf("SHM_WRITE: CHILD: Writing Done\n");
else if (pid > 0)
printf("SHM_WRITE: PARENT: Writing Done\n");
return;
}
void remove_semaphore() {
int semid;
int retval;
semid = semget(SEM_KEY, 1, 0);
if (semid < 0) {
perror("Remove Semaphore: Semaphore GET: ");
return;
}
retval = semctl(semid, 0, IPC_RMID);
if (retval == -1) {
perror("Remove Semaphore: Semaphore CTL: ");
return;
}
return;
}
Étapes de compilation et d'exécution
Total Count is 10000
First Process
SHM_WRITE: PARENT: Now writing
Second Process
SHM_WRITE: PARENT: Writing Done
SHM_WRITE: CHILD: Now writing
SHM_WRITE: CHILD: Writing Done
Writing Process: Complete
Maintenant, nous allons vérifier la valeur du compteur par le processus de lecture.
Étapes d'exécution
Reading Process: Shared Memory: Counter is 20000
Reading Process: Reading Done, Detaching Shared Memory
Reading Process: Complete