Comunicazione tra processi - semafori

La prima domanda che mi viene in mente è: perché abbiamo bisogno dei semafori? Una risposta semplice, per proteggere la regione critica / comune condivisa tra più processi.

Supponiamo che più processi utilizzino la stessa regione di codice e se tutti vogliono accedere parallelamente, il risultato è sovrapposto. Supponiamo, ad esempio, che più utenti utilizzino una sola stampante (sezione comune / critica), diciamo 3 utenti, dati 3 lavori contemporaneamente, se tutti i lavori iniziano parallelamente, l'output di un utente viene sovrapposto a un altro. Quindi, dobbiamo proteggerlo usando i semafori, cioè bloccando la sezione critica quando un processo è in esecuzione e sbloccando quando è finito. Questo verrà ripetuto per ogni utente / processo in modo che un lavoro non sia sovrapposto a un altro lavoro.

Fondamentalmente i semafori sono classificati in due tipi:

Binary Semaphores - Solo due stati 0 e 1, ovvero bloccato / sbloccato o disponibile / non disponibile, implementazione Mutex.

Counting Semaphores - I semafori che consentono il conteggio arbitrario delle risorse sono chiamati semafori di conteggio.

Supponiamo di avere 5 stampanti (per capire supponiamo che 1 stampante accetti solo 1 lavoro) e abbiamo 3 lavori da stampare. Ora verranno assegnati 3 lavori per 3 stampanti (1 ciascuno). Anche in questo caso sono arrivati ​​4 lavori mentre questo è in corso. Ora, su 2 stampanti disponibili, 2 lavori sono stati programmati e ci rimangono altri 2 lavori, che sarebbero completati solo dopo che una delle risorse / stampante sarà disponibile. Questo tipo di pianificazione in base alla disponibilità delle risorse può essere visto come semafori di conteggio.

Per eseguire la sincronizzazione utilizzando i semafori, di seguito sono riportati i passaggi:

Step 1 - Crea un semaforo o connettiti a un semaforo già esistente (semget ())

Step 2 - Eseguire operazioni sul semaforo, ad esempio allocare o rilasciare o attendere le risorse (semop ())

Step 3 - Esegue operazioni di controllo sulla coda dei messaggi (semctl ())

Ora, controlliamo questo con le chiamate di sistema che abbiamo.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

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

Questa chiamata di sistema crea o alloca un set di semafori System V. Devono essere passati i seguenti argomenti:

  • Il primo argomento, key, riconosce la coda dei messaggi. La chiave può essere un valore arbitrario o uno che può essere derivato dalla funzione di libreria ftok ().

  • Il secondo argomento, nsems, specifica il numero di semafori. Se binario allora è 1, implica la necessità di 1 set di semafori, altrimenti secondo il conteggio richiesto del numero di set di semafori.

  • Il terzo argomento, semflg, specifica il / i flag / i semaforo richiesto / i come IPC_CREAT (creazione del semaforo se non esiste) o IPC_EXCL (usato con IPC_CREAT per creare il semaforo e la chiamata fallisce, se esiste già un semaforo). È necessario passare anche le autorizzazioni.

Note - Fare riferimento alle sezioni precedenti per i dettagli sulle autorizzazioni.

Questa chiamata restituirà un identificatore di semaforo valido (usato per ulteriori chiamate di semafori) in caso di successo e -1 in caso di fallimento. Per conoscere la causa del fallimento, controlla con la variabile errno o la funzione perror ().

Vari errori rispetto a questa chiamata sono EACCESS (autorizzazione negata), EEXIST (la coda esiste già non può creare), ENOENT (la coda non esiste), ENOMEM (memoria insufficiente per creare la coda), ENOSPC (limite massimo di set superato), ecc.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *semops, size_t nsemops)

Questa chiamata di sistema esegue le operazioni sui set di semafori System V, vale a dire, allocare risorse, attendere le risorse o liberare le risorse. Devono essere passati i seguenti argomenti:

  • Il primo argomento, semid, indica l'identificatore del set di semafori creato da semget ().

  • Il secondo argomento, semops, è il puntatore a un array di operazioni da eseguire sul set di semafori. La struttura è la seguente:

struct sembuf {
   unsigned short sem_num; /* Semaphore set num */
   short sem_op; /* Semaphore operation */
   short sem_flg; /* Operation flags, IPC_NOWAIT, SEM_UNDO */
};

L'elemento sem_op, nella struttura sopra, indica l'operazione che deve essere eseguita -

  • Se sem_op è –ve, alloca o ottieni risorse. Blocca il processo chiamante finché non sono state liberate risorse sufficienti da altri processi, in modo che questo processo possa allocare.

  • Se sem_op è zero, il processo chiamante attende o dorme finché il valore del semaforo non raggiunge 0.

  • Se sem_op è + ve, rilascia le risorse.

Ad esempio:

struct sembuf sem_lock = {0, -1, SEM_UNDO};

struct sembuf sem_unlock = {0, 1, SEM_UNDO};

  • Il terzo argomento, nsemops, è il numero di operazioni in quell'array.

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, …)

Questa chiamata di sistema esegue un'operazione di controllo per un semaforo di System V. Devono essere passati i seguenti argomenti:

  • Il primo argomento, semid, è l'identificatore del semaforo. Questo id è l'identificatore del semaforo, che è il valore di ritorno della chiamata di sistema semget ().

  • Il secondo argomento, semnum, è il numero di semaforo. I semafori sono numerati da 0.

  • Il terzo argomento, cmd, è il comando per eseguire l'operazione di controllo richiesta sul semaforo.

  • Il quarto argomento, di tipo, union semun, dipende dal cmd. Per pochi casi, il quarto argomento non è applicabile.

Controlliamo l'unione 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 struttura dati semid_ds definita in sys / sem.h è la seguente:

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 - Fare riferimento alle pagine di manuale per altre strutture dati.

union semun arg; I valori validi per cmd sono -

  • IPC_STAT- Copia le informazioni dei valori correnti di ogni membro di struct semid_ds nella struttura passata indicata da arg.buf. Questo comando richiede l'autorizzazione di lettura per il semaforo.

  • IPC_SET - Imposta l'ID utente, l'ID gruppo del proprietario, i permessi, ecc. Puntati dalla struttura semid_ds.

  • IPC_RMID - Rimuove i semafori impostati.

  • IPC_INFO - Restituisce le informazioni sui limiti e sui parametri del semaforo nella struttura semid_ds indicata da arg .__ buf.

  • SEM_INFO - Restituisce una struttura seminfo contenente informazioni sulle risorse di sistema consumate dal semaforo.

Questa chiamata restituirà un valore (valore non negativo) a seconda del comando passato. In caso di successo, IPC_INFO e SEM_INFO o SEM_STAT restituiscono l'indice o l'identificatore della voce più alta utilizzata come da Semaphore o il valore di semncnt per GETNCNT o il valore di sempid per GETPID o il valore di semval per GETVAL 0 per altre operazioni con successo e - 1 in caso di guasto. Per conoscere la causa del fallimento, controlla con la variabile errno o la funzione perror ().

Prima di esaminare il codice, comprendiamo la sua implementazione:

  • Crea due processi diciamo, figlio e genitore.

  • Crea memoria condivisa principalmente necessaria per memorizzare il contatore e altri flag per indicare la fine del processo di lettura / scrittura nella memoria condivisa.

  • Il contatore viene incrementato del conteggio da entrambi i processi padre e figlio. Il conteggio viene passato come argomento della riga di comando o preso come predefinito (se non è passato come argomento della riga di comando o il valore è inferiore a 10000). Chiamato con un certo tempo di sonno per garantire che sia il genitore che il bambino accedano alla memoria condivisa contemporaneamente, cioè in parallelo.

  • Poiché il contatore viene incrementato in passi di 1 sia dal genitore che dal figlio, il valore finale dovrebbe essere il doppio del contatore. Poiché entrambi i processi padre e figlio eseguono le operazioni contemporaneamente, il contatore non viene incrementato come richiesto. Quindi, dobbiamo garantire la completezza del completamento di un processo seguito da un altro processo.

  • Tutte le implementazioni precedenti vengono eseguite nel file shm_write_cntr.c

  • Verificare se il valore del contatore è implementato nel file shm_read_cntr.c

  • Per garantire il completamento, il programma semaforo è implementato nel file shm_write_cntr_with_sem.c. Rimuovere il semaforo dopo il completamento dell'intero processo (dopo che la lettura è stata eseguita da un altro programma)

  • Poiché abbiamo file separati per leggere il valore di counter nella memoria condivisa e non abbiamo alcun effetto dalla scrittura, il programma di lettura rimane lo stesso (shm_read_cntr.c)

  • È sempre meglio eseguire il programma di scrittura in un terminale e leggere il programma da un altro terminale. Poiché il programma completa l'esecuzione solo dopo che il processo di scrittura e lettura è completo, è consentito eseguire il programma dopo aver eseguito completamente il programma di scrittura. Il programma di scrittura attende fino a quando il programma di lettura viene eseguito e termina solo dopo che è stato eseguito.

Programmi senza semafori.

/* 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;
}

Fasi di compilazione ed esecuzione

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

Ora, controlliamo il programma di lettura della memoria condivisa.

/* 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;
}

Fasi di compilazione ed esecuzione

Reading Process: Shared Memory: Counter is 11000
Reading Process: Reading Done, Detaching Shared Memory
Reading Process: Complete

Se si osserva l'output di cui sopra, il contatore dovrebbe essere 20000, tuttavia, poiché prima del completamento di un'attività di processo anche un altro processo viene elaborato in parallelo, il valore del contatore non è quello previsto. L'output varierebbe da sistema a sistema e inoltre varierebbe con ogni esecuzione. Per garantire che i due processi eseguano l'attività dopo il completamento di un'attività, è necessario implementarla utilizzando meccanismi di sincronizzazione.

Ora, controlliamo la stessa applicazione usando i semafori.

Note - Il programma di lettura rimane lo stesso.

/* 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;
}

Fasi di compilazione ed esecuzione

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

Ora, controlleremo il valore del contatore tramite il processo di lettura.

Fasi di esecuzione

Reading Process: Shared Memory: Counter is 20000
Reading Process: Reading Done, Detaching Shared Memory
Reading Process: Complete