侧边栏壁纸
  • 累计撰写 32 篇文章
  • 累计创建 55 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

Linux IPC:Semaphores(信号量)

Testerfans
2022-06-02 / 0 评论 / 20 点赞 / 981 阅读 / 14,902 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-06-29,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

本文演示 CentOS 7.6:Linux testerfans 3.10.0-1160.45.1.el7.x86_64

前言

前面我们介绍了(无名)管道命名管道共享内存消息队列来实现相关和无关进程进程之间的通信。本章开始我们将继续介绍信号量。

首先我们思考一下,为什么我们需要使用信号量呢?一个简单的答案就是保护多个进程之间的共享访问空间或资源。

我们假设多个进程如果并行访问相同的内存资源并向内写入,在不控制的情况下会造成两个进程写入的信息重叠或者覆盖。例如,多个用户使用一台打印机(公共/关键区),如果有3 个用户并同时提交 3个打印作业,如果所有任务并行执行,则一个用户输出与另一个用户输出重叠(打印到了一起)。因此,我们需要使用信号量来保护它,即在一个进程运行时锁定关键部分,并在完成时解锁。锁定/解锁动作对每个用户/进程重复,以便一个作业不会与另一个作业重叠。

信号量大体上分为两种类型:

  • 二进制信号量(Binary Semaphores):只有0 和 1两种状态,即锁定/解锁或可用/不可用,用来实现互斥锁。
  • 计数信号量(Counting Semaphores):允许任意资源计数的信号量称为计数信号量。

假设我们有 5 台打印机(设定 1 台打印机只接受 1 个作业),我们有 3 个作业要打印。现在将为 3 台打印机(每台 1 个作业)分配 3 个作业。在此过程新增 4 个作业。现在,给剩余的 2 台分配 2 个作业,剩余 2 个作业,只有在其中任意一台资源/打印机可用后才能继续分配。这种根据资源可用性进行的调度可以看作是计数信号量。

信号量的特点

  • 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
  • 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
  • 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
  • 支持信号量组。

系统调用

  • 第 1 步- 创建消息队列或连接到已经存在的信号量 (semget())。
  • 第 2 步- 对信号量执行申请、释放或等待操作(semop())。
  • 第 3 步- 对信号量执行控制操作 (semctl())。

semget()

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

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

semget()创建或分配一个 System V 信号量集。

传参

  • key:信号量标识,key可以是任意值,也可以是从库函数 ftok() 派生的值。
  • nsems:指定信号量的数量。如果二进制则为 1,意味着需要 1 个信号量集,否则根据所需的信号量集数量计数。
  • semflg:是权限标志,它的作用与open函数的mode参数一样。

如果 semflg 同时指定了 IPC_CREAT 和 IPC_EXCL 并且指定key的信号量已经存在,则 semget() 将失败,并将 errno 设置为 EEXIST。 (这类似于组合 O_CREAT | O_EXCL 对 open(2) 的影响。)。

返回

  • 调用成功时返回一个信号量标识。
  • -1:失败。

错误返回

错误标识 描述
EACCES key 指定的信号量集已经存在,但调用进程无权访问该信号量集,也没有 CAP_IPC_OWNER 能力。
EEXIST key 指定的信号量集已经存在,且semflg指定了IPC_CREAT 和 IPC_EXCL。
EINVAL nsems 小于 0 或大于每个信号量集的信号量限制 (SEMMSL),或者与 key 对应的信号量集已经存在,并且 nsems 大于该集合中的信号量数。
ENOENT key 指定的信号量集不存在,并且 semflg 没有指定 IPC_CREAT。
ENOMEM 必须创建但没有足够的内存创建信号量集合。
ENOSPC 必须创建但会超出最大信号量集数 (SEMMNI) 或系统范围最大信号量数 (SEMMNS) 的系统限制。

semop()

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

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

该系统调用在 System V 信号量集上执行操作,即分配资源、等待资源或释放资源。

传参

  • semid:由 semget() 创建的信号量集标识符。
  • semops:指向要在信号量集上执行的操作数组的指针。在Linux 的系统库linux/sem.h 中,结构如下:
struct sembuf  {  
	unsigned short int sem_num;   /* 信号量的序号从0~nsems-1 */  
	short int sem_op;             /* 对信号量的操作,>0, 0, <0 */  
	short int sem_flg;            /* 操作标识:0, IPC_WAIT, SEM_UNDO */  
};

上述结构中的元素 sem_op 表示需要执行的操作:

  1. 如果 sem_op 为 < 0,则分配或获取资源。阻塞调用进程,直到其他进程释放了足够的资源。
  2. 如果 sem_op 为 = 0,则调用进程等待或休眠,直到信号量值达到 0。
  3. 如果 sem_op 为 > 0,则释放资源。

sem_flg 中识别的标志是 IPC_NOWAIT 和 SEM_UNDO。如果一个操作指定了 SEM_UNDO,它将在进程终止时自动撤消。

例如:

struct sembuf sem_lock = { 0, -1, SEM_UNDO };
struct sembuf sem_unlock = {0, 1, SEM_UNDO };
  • nsemops:该数组中的操作数。

返回

  • 0:成功。
  • -1:失败。

错误返回

错误标识 描述
E2BIG 参数 nsops 大于 SEMOPM,即每个系统调用允许的最大操作数。
EACCES 调用进程没有执行指定信号量操作所需的权限,也没有 CAP_IPC_OWNER 能力。
EAGAIN 操作无法立即进行,并且在 sem_flg 中指定了 IPC_NOWAIT 或在 timeout 中指定的时间已超时。
EFAULT 在 sops 或 timeout 参数中指定的地址不可访问。
EFBIG 对于某些操作,sem_num 的值小于 0 或大于或等于集合中的信号量数。
EIDRM 信号量集合已经被删除。
EINTR 在这个系统调用中被阻塞时,线程捕捉到了一个信号,见signal(7)。
EINVAL 信号量集不存在,或者 semid 小于零,或者 nsops 具有非正值。
ENOMEM 某些操作的 sem_flg 指定了 SEM_UNDO 并且系统没有足够的内存来分配 undo 结构。
ERANGE 对于某些操作,sem_op+semval 大于 SEMVMX,即 semval 的实现相关的最大值。

msgctl()

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

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

semctl() 对由 semid 标识的 System V 信号量集或该集合的第 semnum 信号量执行 cmd 指定的控制操作。

传参

  • semid:信号量的标识符。semget()系统调用的返回值。
  • semnum:信号量的个数。信号量从 0 开始编号。
  • cmd:对信号量执行所需控制操作的命令。
CMD 描述
IPC_STAT 将与 semid 关联的内核数据结构中的信息复制到 arg.buf 指向的 semid_ds 结构中。参数 semnum 被忽略。调用进程必须对信号量集具有读取权限。
IPC_SET 将 arg.buf 指向的 semid_ds 结构的某些成员的值写入与此信号量集关联的内核数据结构,同时更新其 sem_ctime 成员。结构的以下成员已更新:sem_perm.uid、sem_perm.gid 和sem_perm.mode。调用进程的有效 UID 必须与信号量集的所有者 (sem_perm.uid) 或创建者 (sem_perm.cuid) 匹配,否则调用者必须具有特权。参数 semnum 被忽略。
IPC_RMID 立即删除信号量集,唤醒所有在 semop(2) 调用中阻塞的进程(返回错误并将 errno 设置为 EIDRM)。调用进程的有效用户 ID 必须与信号量集的创建者或所有者匹配,否则调用者必须具有特权。参数 semnum 被忽略。
IPC_INFO 在 arg.__buf 指向的结构中返回有关系统范围的信号量限制和参数的信息。如果定义了 _GNU_SOURCE 功能测试宏,则此结构是 seminfo 类型,在 <sys/sem.h> 中定义参考struct seminfo
SEM_INFO 返回一个包含与 IPC_INFO 相同信息的 seminfo 结构,除了以下字段返回有关信号量消耗的系统资源的信息: semusz 字段返回系统上当前存在的信号量集的数量; semaem 字段返回系统上所有信号量集中的信号量总数。
SEM_STAT 返回与 IPC_STAT 相同的 semid_ds 结构。但是, semid 参数不是信号量标识符,而是内核内部数组的索引,该数组维护有关系统上所有信号量集的信息。
GETALL 将集合中所有信号量的 semval(即当前值)返回到 arg.array。参数 semnum 被忽略。调用进程必须对信号量集具有读取权限。
GETNCNT 系统调用返回集合的第 semnum 信号量的 semncnt 值(即,等待集合的第 semnum 信号量增加 semval 的进程数)。调用进程必须对信号量集具有读取权限。
GETPID 系统调用为集合的第 semnum 信号量返回 sempid 的值(即,为集合的第 semnum 信号量执行最后一次 semop(2) 调用的进程的 PID)。调用进程必须对信号量集具有读取权限。
GETVAL 系统调用返回集合中第 semnum 信号量的 semval 值。调用进程必须对信号量集具有读取权限。
GETZCNT 系统调用返回集合的第 semnum 信号量的 semzcnt 值(即等待集合的第 semnum 信号量的 semval 变为 0 的进程数)。调用进程必须对信号量集具有读取权限。
SETALL 使用 arg.array 为集合的所有信号量设置 semval,同时更新与集合关联的 semid_ds 结构的 sem_ctime 成员。撤消条目(请参阅 semop(2))清除所有进程中更改的信号量。如果对信号量值的更改允许其他进程中的阻塞 semop(2) 调用继续进行,那么这些进程将被唤醒。参数 semnum 被忽略。调用进程必须对信号量集具有更改(写入)权限。
SETVAL 对于集合的第 semnum 信号量,将 semval 的值设置为 arg.val,同时更新与集合关联的 semid_ds 结构的 sem_ctime 成员。清除所有进程中更改的信号量的撤消条目。如果对信号量值的更改允许其他进程中的阻塞 semop(2) 调用继续进行,那么这些进程将被唤醒。调用进程必须对信号量集具有更改权限。

seminfo在 <sys/sem.h> 中为我们声明如下:

struct  seminfo {
	int semmap;  /* 信号量映射中的条目数;在内核中未使用 */
	int semmni;  /* 信号量集的最大数量 */
	int semmns;  /* 所有信号量集中的最大信号量数 */
	int semmnu;  /* 系统范围的最大撤消结构数;在内核中未使用 */
	int semmsl;  /* 一组信号量的最大数量 */
	int semopm;  /* semop(2) 的最大操作数 */
	int semume;  /* 每个进程的最大撤消条目数;在内核中未使用 */
	int semusz;  /* 结构 sem_undo 的大小 */
	int semvmx;  /* 最大信号量值 */
	int semaem;  /* 信号量调整可以被记录的最大值(SEM_UNDO)*/
};

semmsl, semmns, semopm和semmni 设置可以在/proc/sys/kernel/sem 进行调整。man 5 proc(5) 查看详细信息。

  • 第四个参数:当有四个时参数时,参数类型为 union semun。调用程序必须按如下方式定义如下union结构:
union semun {
	int              val;    /* SETVAL的值 */
	struct semid_ds *buf;    /* IPC_STAT、IPC_SET 的缓冲区 */
	unsigned short  *array;  /* GETALL、SETALL 的数组 */
	struct seminfo  *__buf;  /* IPC_INFO 的缓冲区(Linux 指定) */
};

semid_ds它在 linux/sem.h 中定义如下:

 struct semid_ds {
	 struct ipc_perm sem_perm;  /* 所有者关系和权限,参考ipc_perm 结构 */
	 time_t          sem_otime; /* 最后一次 semop() 操作的时间 */
	 time_t          sem_ctime; /* 上次更改此结构的时间 */
	 unsigned long   sem_nsems; /* 信号量集合(数组)中的信号量数量 */
 };

内核将 IPC 对象的权限信息存储在ipc_perm类型的结构中。在linux/ipc.h中为我们声明如下:

struct ipc_perm {
	key_t          __key;    /* 对象的键值,调用shmget返回的键值*/
	uid_t          uid;      /* 所有者的有效用户ID */
	gid_t          gid;      /* 所有者的有效组ID */
	uid_t          cuid;     /* 创建者的有效用户ID*/
	gid_t          cgid;     /* 创建者的有效组ID*/
	unsigned short mode;     /* Permissions + SHM_DEST和SHM_LOCKED标志*/
	unsigned short __seq;    /* 对象的序列号 */
	};

返回

  • 0,成功。除如下根据cmd指定返回不同的值外。
CMD 返回值
IPC_INFO 内核内部数组中记录所有信号量集信息的最高使用条目的索引。 (此信息可与重复的 SEM_STAT 操作一起使用,以获取有关系统上所有信号量集的信息。)
SEM_INFO 返回一个包含与 IPC_INFO 相同信息的 seminfo 结构,除了以下字段返回有关信号量消耗的系统资源的信息: semusz 字段返回系统上当前存在的信号量集的数量; semaem 字段返回系统上所有信号量集中的信号量总数。
SEM_STAT 返回与 IPC_STAT 相同的 semid_ds 结构。但是, semid 参数不是信号量标识符,而是内核内部数组的索引,该数组维护有关系统上所有信号量集的信息。
GETNCNT 系统调用返回集合的第 semnum 信号量的 semncnt 值(即,等待集合的第 semnum 信号量增加 semval 的进程数)。调用进程必须对信号量集具有读取权限。
GETPID 系统调用为集合的第 semnum 信号量返回 sempid 的值(即,为集合的第 semnum 信号量执行最后一次 semop(2) 调用的进程的 PID)。调用进程必须对信号量集具有读取权限。
GETVAL 系统调用返回集合中第 semnum 信号量的 semval 值。调用进程必须对信号量集具有读取权限。
GETZCNT 系统调用返回集合的第 semnum 信号量的 semzcnt 值(即等待集合的第 semnum 信号量的 semval 变为 0 的进程数)。调用进程必须对信号量集具有读取权限。
  • -1:失败。

错误返回

错误标识 描述
EACCES 参数 cmd 具有值 GETALL、GETPID、GETVAL、GETNCNT、GETZCNT、IPC_STAT、SEM_STAT、SETALL 或 SETVAL 之一,并且调用进程对信号量集没有所需的权限,并且没有 CAP_IPC_OWNER 能力。
EFAULT arg.buf 或 arg.array 指向的地址不可访问。
EIDRM 信号量集被删除。
EINVAL cmd 或 semid 的值无效。或者:对于 SEM_STAT 操作,在 semid 中指定的索引值引用了当前未使用的数组槽。
EPERM 参数 cmd 的值为 IPC_SET 或 IPC_RMID 但调用进程的有效用户 ID 不是信号量集的创建者(在 sem_perm.cuid 中找到)或所有者(在 sem_perm.uid 中找到),并且进程没有 CAP_SYS_ADMIN 能力。
ERANGE 参数 cmd 具有值 SETALL 或 SETVAL 并且要设置 semval 的值(对于集合的某些信号量)小于 0 或大于实现限制 SEMVMX。

示例程序

我们实现三个程序,一个是在使用信号量情况下两个进程同时像共享内存以增量1更改计数器,另外一个则使用信号量控制。第三个是从共享内存中进行读取。

无信号量控制情况下,多进程同时向一块共享内存写入情况:

  • 创建两个程序,一个写程序shm_write_cntr.c,一个读程序shm_read_cntr.c。
  • 创建共享内存用来存储计数器和end标志,以标记对共享内存读/写结束。
  • 计数器由写程序内的父进程和子进程按计数递增。
    • 计数作为命令行参数传递
    • 使用默认值为10000。
  • 以一定的睡眠时间间隔调用,以确保父进程和子进程同时(并行)访问共享内存。
  • 在两个shell中先执行shm_write_cntr.c,再执行shm_read_cntr.c,检查读出的计数器值。

增加信号量控制,多进程同时向一块共享内存写入情况:

  • 创建一个带信号量控制的程序 shm_write_cntr_with_sem.c,控制父进程和子进程写入。
  • 读程序依然使用shm_read_cntr.c
  • 在两个shell中先执行shm_write_cntr_with_sem.c,在执行shm_read_cntr.c,检查读出的计数器值。

源代码
shm_write_cntr.c

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

shm_write_cntr_with_sem.c

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

shm_read_cntr.c

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

编译

[root@testerfans namespace]# gcc shm_write_cntr.c -o shm_write_cntr          
[root@testerfans namespace]# gcc shm_read_cntr.c -o shm_read_cntr
[root@testerfans namespace]# gcc shm_write_cntr_with_sem.c -o shm_write_cntr_with_sem

执行/输出

  • 我们首先打开两个shell,查看在没有使用信号量时向共享内存写入的情况。
  • 之后查看使用信号量控制写入的情况。

image-1654178527419

从图中执行的情况下可以看出,没有使用信号量写入的时候实际结果并不是预期的20000,而是11000。使用信号量的时候与预期结果一致。说明信号量在并行访问相同一块共享内存时候起到了控制的作用。

总结

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。


本文参考:
Inter Process Communication - Semaphores
man手册

20

评论区