Comunicação entre processos - semáforos

A primeira pergunta que vem à mente é: por que precisamos de semáforos? Uma resposta simples para proteger a região crítica / comum compartilhada entre vários processos.

Vamos supor que vários processos estejam usando a mesma região de código e se todos quiserem acessar paralelamente, o resultado será sobreposto. Digamos, por exemplo, que vários usuários estejam usando apenas uma impressora (seção comum / crítica), digamos 3 usuários, dados 3 trabalhos ao mesmo tempo, se todos os trabalhos começarem paralelamente, uma saída do usuário será sobreposta a outra. Portanto, precisamos proteger isso usando semáforos, ou seja, bloqueando a seção crítica quando um processo está em execução e desbloqueando quando estiver concluído. Isso seria repetido para cada usuário / processo, de modo que um trabalho não seja sobreposto a outro.

Basicamente, os semáforos são classificados em dois tipos -

Binary Semaphores - Apenas dois estados 0 e 1, ou seja, bloqueado / desbloqueado ou disponível / indisponível, implementação Mutex.

Counting Semaphores - Os semáforos que permitem a contagem arbitrária de recursos são chamados de semáforos de contagem.

Suponha que temos 5 impressoras (para entender, suponha que 1 impressora aceita apenas 1 trabalho) e temos 3 trabalhos para imprimir. Agora, 3 trabalhos seriam dados para 3 impressoras (1 cada). Novamente, 4 trabalhos surgiram enquanto isso estava em andamento. Agora, das 2 impressoras disponíveis, 2 trabalhos foram programados e ficamos com mais 2 trabalhos, que seriam concluídos somente após um dos recursos / impressora estar disponível. Este tipo de agendamento de acordo com a disponibilidade de recursos pode ser visto como semáforos de contagem.

Para realizar a sincronização usando semáforos, a seguir estão as etapas -

Step 1 - Crie um semáforo ou conecte-se a um semáforo já existente (semget ())

Step 2 - Executar operações no semáforo, ou seja, alocar, liberar ou aguardar os recursos (semop ())

Step 3 - Executar operações de controle na fila de mensagens (semctl ())

Agora, vamos verificar isso com as chamadas de sistema que temos.

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

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

Esta chamada de sistema cria ou aloca um conjunto de semáforo System V. Os seguintes argumentos precisam ser passados ​​-

  • O primeiro argumento, chave, reconhece a fila de mensagens. A chave pode ser um valor arbitrário ou derivado da função de biblioteca ftok ().

  • O segundo argumento, nsems, especifica o número de semáforos. Se binário, então é 1, implica a necessidade de 1 conjunto de semáforo, caso contrário, de acordo com a contagem necessária do número de conjuntos de semáforo.

  • O terceiro argumento, semflg, especifica os sinalizadores de semáforo necessários, como IPC_CREAT (criando semáforo se ele não existir) ou IPC_EXCL (usado com IPC_CREAT para criar o semáforo e a chamada falhar, se um semáforo já existir). Precisa passar as permissões também.

Note - Consulte as seções anteriores para obter detalhes sobre as permissões.

Esta chamada retornaria um identificador de semáforo válido (usado para chamadas adicionais de semáforos) em caso de sucesso e -1 em caso de falha. Para saber a causa da falha, verifique com a variável errno ou função perror ().

Vários erros em relação a esta chamada são EACCESS (permissão negada), EEXIST (fila já existe, não pode ser criada), ENOENT (fila não existe), ENOMEM (memória insuficiente para criar a fila), ENOSPC (limite 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 chamada de sistema executa as operações nos conjuntos de semáforo do System V, a saber, alocar recursos, aguardar os recursos ou liberar os recursos. Os seguintes argumentos precisam ser passados ​​-

  • O primeiro argumento, semid, indica o identificador do conjunto de semáforo criado por semget ().

  • O segundo argumento, semops, é o ponteiro para uma matriz de operações a serem realizadas no conjunto do semáforo. A estrutura é a seguinte -

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

Elemento, sem_op, na estrutura acima, indica a operação que precisa ser realizada -

  • Se sem_op for –ve, aloque ou obtenha recursos. Bloqueia o processo de chamada até que recursos suficientes sejam liberados por outros processos, para que este processo possa ser alocado.

  • Se sem_op for zero, o processo de chamada espera ou dorme até que o valor do semáforo chegue a 0.

  • Se sem_op for + ve, libere recursos.

Por exemplo -

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

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

  • O terceiro argumento, nsemops, é o número de operações nessa matriz.

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

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

Esta chamada de sistema executa a operação de controle para um semáforo System V. Os seguintes argumentos precisam ser passados ​​-

  • O primeiro argumento, semid, é o identificador do semáforo. Este id é o identificador do semáforo, que é o valor de retorno da chamada do sistema semget ().

  • O segundo argumento, semnum, é o número do semáforo. Os semáforos são numerados a partir de 0.

  • O terceiro argumento, cmd, é o comando para executar a operação de controle necessária no semáforo.

  • O quarto argumento, do tipo, união semun, depende do cmd. Para alguns casos, o quarto argumento não é aplicável.

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

A estrutura de dados semid_ds que é definida em sys / sem.h é a seguinte -

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 as páginas do manual para outras estruturas de dados.

união semun arg; Os valores válidos para cmd são -

  • IPC_STAT- Copia as informações dos valores atuais de cada membro de struct semid_ds para a estrutura passada apontada por arg.buf. Este comando requer permissão de leitura para o semáforo.

  • IPC_SET - Define o ID do usuário, ID do grupo do proprietário, permissões, etc. apontados pela estrutura semid_ds.

  • IPC_RMID - Remove o conjunto de semáforos.

  • IPC_INFO - Retorna a informação sobre os limites e parâmetros do semáforo na estrutura semid_ds apontada por arg .__ buf.

  • SEM_INFO - Retorna uma estrutura seminfo contendo informações sobre os recursos do sistema consumidos pelo semáforo.

Esta chamada retornaria um valor (valor não negativo) dependendo do comando passado. Após o sucesso, IPC_INFO e SEM_INFO ou SEM_STAT retorna o índice ou identificador da entrada mais alta usada de acordo com o Semáforo ou o valor de semncnt para GETNCNT ou o valor de sempid para GETPID ou o valor de semval para GETVAL 0 para outras operações com sucesso e - 1 em caso de falha. Para saber a causa da falha, verifique com a variável errno ou função perror ().

Antes de examinar o código, vamos entender sua implementação -

  • Crie dois processos, digamos, filho e pai.

  • Crie a memória compartilhada necessária principalmente para armazenar o contador e outros sinalizadores para indicar o fim do processo de leitura / gravação na memória compartilhada.

  • O contador é incrementado pela contagem pelos processos pai e filho. A contagem é passada como um argumento de linha de comando ou considerada como padrão (se não for passada como um argumento de linha de comando ou o valor for menor que 10.000). Chamado com certo tempo de sono para garantir que ambos, pai e filho, acessem a memória compartilhada ao mesmo tempo, ou seja, em paralelo.

  • Como o contador é incrementado em etapas de 1 tanto pelo pai quanto pelo filho, o valor final deve ser o dobro do contador. Visto que os processos pai e filho executam as operações ao mesmo tempo, o contador não é incrementado conforme necessário. Portanto, precisamos garantir a integridade de uma conclusão de processo seguida por outro processo.

  • Todas as implementações acima são realizadas no arquivo shm_write_cntr.c

  • Verifique se o valor do contador está implementado no arquivo shm_read_cntr.c

  • Para garantir a conclusão, o programa semáforo é implementado no arquivo shm_write_cntr_with_sem.c. Remova o semáforo após a conclusão de todo o processo (após a leitura de outro programa)

  • Uma vez que temos arquivos separados para ler o valor do contador na memória compartilhada e não temos nenhum efeito da escrita, o programa de leitura permanece o mesmo (shm_read_cntr.c)

  • É sempre melhor executar o programa de escrita em um terminal e ler o programa em outro terminal. Uma vez que o programa conclui a execução somente depois que o processo de gravação e leitura é concluído, está ok para executar o programa depois de executar completamente o programa de gravação. O programa de gravação esperaria até que o programa de leitura fosse executado e só terminaria depois de concluído.

Programas sem 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;
}

Etapas de compilação e execução

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

Agora, vamos verificar o programa de leitura de memória compartilhada.

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

Etapas de compilação e execução

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

Se você observar a saída acima, o contador deve ser 20000, no entanto, como antes da conclusão de uma tarefa de processo outro processo também está processando em paralelo, o valor do contador não é o esperado. A saída varia de sistema para sistema e também varia com cada execução. Para garantir que os dois processos executem a tarefa após a conclusão de uma tarefa, ele deve ser implementado usando mecanismos de sincronização.

Agora, vamos verificar o mesmo aplicativo usando semáforos.

Note - O programa de leitura permanece o mesmo.

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

Etapas de compilação e execução

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

Agora, vamos verificar o valor do contador pelo processo de leitura.

Etapas de execução

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