Comunicación entre procesos: semáforos

La primera pregunta que me viene a la mente es, ¿por qué necesitamos semáforos? Una respuesta simple, para proteger la región crítica / común compartida entre múltiples procesos.

Supongamos que varios procesos están usando la misma región de código y si todos quieren acceder en paralelo, el resultado se superpone. Digamos, por ejemplo, que varios usuarios están usando una sola impresora (sección común / crítica), digamos 3 usuarios, dados 3 trabajos al mismo tiempo, si todos los trabajos comienzan en paralelo, entonces una salida de usuario se superpone con otra. Por lo tanto, necesitamos proteger eso usando semáforos, es decir, bloqueando la sección crítica cuando un proceso se está ejecutando y desbloqueando cuando termina. Esto se repetirá para cada usuario / proceso para que un trabajo no se superponga con otro trabajo.

Básicamente, los semáforos se clasifican en dos tipos:

Binary Semaphores - Solo dos estados 0 y 1, es decir, bloqueado / desbloqueado o disponible / no disponible, implementación de Mutex.

Counting Semaphores - Los semáforos que permiten el recuento arbitrario de recursos se denominan semáforos de recuento.

Suponga que tenemos 5 impresoras (para entender, suponga que 1 impresora solo acepta 1 trabajo) y tenemos 3 trabajos para imprimir. Ahora se darían 3 trabajos para 3 impresoras (1 cada una). Nuevamente llegaron 4 trabajos mientras esto estaba en progreso. Ahora, de 2 impresoras disponibles, se han programado 2 trabajos y nos quedan 2 trabajos más, que se completarían solo después de que uno de los recursos / impresora esté disponible. Este tipo de programación según la disponibilidad de recursos puede verse como un conteo de semáforos.

Para realizar la sincronización usando semáforos, los siguientes son los pasos:

Step 1 - Cree un semáforo o conéctese a un semáforo ya existente (semget ())

Step 2 - Realizar operaciones en el semáforo, es decir, asignar o liberar o esperar los recursos (semop ())

Step 3 - Realizar operaciones de control en la cola de mensajes (semctl ())

Ahora, comprobemos esto con las llamadas al sistema que tenemos.

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

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

Esta llamada al sistema crea o asigna un conjunto de semáforos del Sistema V. Se deben pasar los siguientes argumentos:

  • El primer argumento, clave, reconoce la cola de mensajes. La clave puede ser un valor arbitrario o uno que se pueda derivar de la función de biblioteca ftok ().

  • El segundo argumento, nsems, especifica el número de semáforos. Si es binario, entonces es 1, implica la necesidad de 1 conjunto de semáforos, de lo contrario, según el recuento requerido del número de conjuntos de semáforos.

  • El tercer argumento, semflg, especifica las banderas de semáforo requeridas, como IPC_CREAT (creando semáforo si no existe) o IPC_EXCL (usado con IPC_CREAT para crear semáforo y la llamada falla, si ya existe un semáforo). También es necesario aprobar los permisos.

Note - Consulte las secciones anteriores para obtener detalles sobre los permisos.

Esta llamada devolvería un identificador de semáforo válido (utilizado para futuras llamadas de semáforos) en caso de éxito y -1 en caso de fallo. Para conocer la causa de la falla, verifique con la variable errno o la función perror ().

Varios errores con respecto a esta llamada son EACCESS (permiso denegado), EEXIST (la cola ya existe no se puede crear), ENOENT (la cola no existe), ENOMEM (no hay suficiente memoria para crear la cola), ENOSPC (límite máximo de conjuntos excedido), etc.

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

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

Esta llamada al sistema realiza las operaciones en los conjuntos de semáforos del Sistema V, es decir, asigna recursos, espera los recursos o libera los recursos. Se deben pasar los siguientes argumentos:

  • El primer argumento, semid, indica el identificador de conjunto de semáforos creado por semget ().

  • El segundo argumento, semops, es el puntero a una matriz de operaciones que se realizarán en el conjunto de semáforos. La estructura es la siguiente:

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

El elemento, sem_op, en la estructura anterior, indica la operación que debe realizarse:

  • Si sem_op es –ve, asigne u obtenga recursos. Bloquea el proceso de llamada hasta que otros procesos hayan liberado suficientes recursos para que este proceso pueda asignar.

  • Si sem_op es cero, el proceso de llamada espera o duerme hasta que el valor del semáforo llega a 0.

  • Si sem_op es + ve, libera recursos.

Por ejemplo

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

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

  • El tercer argumento, nsemops, es el número de operaciones en esa matriz.

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

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

Esta llamada al sistema realiza una operación de control para un semáforo del Sistema V. Se deben pasar los siguientes argumentos:

  • El primer argumento, semid, es el identificador del semáforo. Este id es el identificador de semáforo, que es el valor de retorno de la llamada al sistema semget ().

  • El segundo argumento, semnum, es el número de semáforo. Los semáforos están numerados desde 0.

  • El tercer argumento, cmd, es el comando para realizar la operación de control requerida en el semáforo.

  • El cuarto argumento, de tipo union semun, depende del cmd. En algunos casos, el cuarto argumento no es aplicable.

Revisemos el sindicato 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 estructura de datos semid_ds que se define en sys / sem.h es la siguiente:

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 - Consulte las páginas del manual para otras estructuras de datos.

union semun arg; Los valores válidos para cmd son:

  • IPC_STAT- Copia la información de los valores actuales de cada miembro de la estructura semid_ds a la estructura pasada señalada por arg.buf. Este comando requiere permiso de lectura para el semáforo.

  • IPC_SET - Establece la ID de usuario, ID de grupo del propietario, permisos, etc. señalados por la estructura semid_ds.

  • IPC_RMID - Elimina el conjunto de semáforos.

  • IPC_INFO - Devuelve la información sobre los límites y parámetros del semáforo en la estructura semid_ds apuntada por arg .__ buf.

  • SEM_INFO - Devuelve una estructura seminfo que contiene información sobre los recursos del sistema consumidos por el semáforo.

Esta llamada devolvería un valor (valor no negativo) dependiendo del comando pasado. Si tiene éxito, IPC_INFO y SEM_INFO o SEM_STAT devuelve el índice o identificador de la entrada utilizada más alta según Semaphore o el valor de semncnt para GETNCNT o el valor de sempid para GETPID o el valor de semval para GETVAL 0 para otras operaciones en caso de éxito y - 1 en caso de avería. Para conocer la causa de la falla, verifique con la variable errno o la función perror ().

Antes de mirar el código, comprendamos su implementación:

  • Cree dos procesos, digamos, hijo y padre.

  • Crear memoria compartida principalmente necesaria para almacenar el contador y otras banderas para indicar el final del proceso de lectura / escritura en la memoria compartida.

  • El contador se incrementa por recuento por los procesos padre e hijo. El recuento se pasa como un argumento de línea de comando o se toma como predeterminado (si no se pasa como argumento de línea de comando o el valor es menor que 10000). Se llama con cierto tiempo de sueño para garantizar que tanto el padre como el niño accedan a la memoria compartida al mismo tiempo, es decir, en paralelo.

  • Dado que tanto el padre como el hijo incrementan el contador en pasos de 1, el valor final debe ser el doble del contador. Dado que los procesos padre e hijo realizan las operaciones al mismo tiempo, el contador no se incrementa según sea necesario. Por lo tanto, debemos garantizar la finalización de un proceso seguido de otro proceso.

  • Todas las implementaciones anteriores se realizan en el archivo shm_write_cntr.c

  • Compruebe si el valor del contador está implementado en el archivo shm_read_cntr.c

  • Para garantizar la finalización, el programa de semáforos se implementa en el archivo shm_write_cntr_with_sem.c. Elimine el semáforo después de completar todo el proceso (después de leer desde otro programa)

  • Dado que tenemos archivos separados para leer el valor del contador en la memoria compartida y no tenemos ningún efecto de escritura, el programa de lectura sigue siendo el mismo (shm_read_cntr.c)

  • Siempre es mejor ejecutar el programa de escritura en un terminal y el programa de lectura desde otro terminal. Dado que el programa completa la ejecución solo después de que se completa el proceso de escritura y lectura, está bien ejecutar el programa después de ejecutar completamente el programa de escritura. El programa de escritura esperará hasta que se ejecute el programa de lectura y solo finalizará una vez hecho.

Programas sin semáforos.

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

Pasos de compilación y ejecución

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

Ahora, revisemos el programa de lectura de memoria compartida.

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

Pasos de compilación y ejecución

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

Si observa la salida anterior, el contador debería ser 20000, sin embargo, ya que antes de completar una tarea de proceso, otro proceso también se está procesando en paralelo, el valor del contador no es el esperado. La salida variaría de un sistema a otro y también variaría con cada ejecución. Para garantizar que los dos procesos realicen la tarea después de completar una tarea, debe implementarse utilizando mecanismos de sincronización.

Ahora, revisemos la misma aplicación usando semáforos.

Note - El programa de lectura sigue siendo el mismo.

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

Pasos de compilación y ejecución

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

Ahora, comprobaremos el valor del contador mediante el proceso de lectura.

Pasos de ejecución

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