本文继《System V IPC 之共享内存》之后接着介绍 System V IPC 的信号量编程。在开始正式的内容前让我们先概要的了解一下 Linux 中信号量的分类。

信号量的分类

在学习 IPC 信号量之前,让我们先来了解一下 Linux 提供两类信号量:

  • 内核信号量,由内核控制路径使用。
  • 用户态进程使用的信号量,这种信号量又分为 POSIX 信号量和 System V 信号量。

POSIX 信号量与 System V 信号量的区别如下:

  • 对 POSIX 来说,信号量是个非负整数,常用于线程间同步。而 System V 信号量则是一个或多个信号量的集合,它对应的是一个信号量结构体,这个结构体是为 System V IPC 服务的,信号量只不过是它的一部分,常用于进程间同步。
  • POSIX 信号量的引用头文件是 "<semaphore.h>",而 System V 信号量的引用头文件是 "<sys/sem.h>"。
  • 从使用的角度,System V 信号量的使用比较复杂,而 POSIX 信号量使用起来相对简单。

本文介绍 System V 信号量编程的基本内容。

System V IPC 信号量

信号量是一种用于对多个进程访问共享资源进行控制的机制。共享资源通常可以分为两大类:

  • 互斥共享资源,即任一时刻只允许一个进程访问该资源
  • 同步共享资源,即同一时刻允许多个进程访问该资源

信号量是为了解决互斥共享资源的同步问题而引入的机制。信号量的实质是整数计数器,其中记录了可供访问的共享资源的单元个数。本文接下来提到的信号量都特指 System V IPC 信号量。

当有进程要求使用某一资源时,系统首先要检测该资源的信号量,如果该资源的信号量的值大于 0,则进程可以使用这一资源,同时信号量的值减 1。进程对资源访问结束时,信号量的值加 1。如果该资源信号量的值等于 0,则进程休眠,直至信号量的值大于 0 时进程被唤醒,访问该资源。
信号量中一种常见的形式是双态信号量。双态信号量对应于只有一个可供访问单元的互斥共享资源,它的初始值被设置为 1,任一时刻至多只允许一个进程对资源进行访问。
信号量用于实现对任意资源的锁定机制。它可以用来同步对任何共享资源的访问。

相关数据结构

System V 子系统提供的信号量机制是比较复杂的。我们不能单独定义一个信号量,而只能定义一个信号量集,其中包括一组信号量,同一信号量集中的信号量可以使用同一 ID 引用。每个信号量集都有一个与其相对应的结构,其中包含了信号量集的各种信息,该结构的声明如下:

struct semid_ds
{
struct ipc_perm sem_perm;
struct sem *sem_base;
ushort sem_nsems;
time_t sem_otime;
time_t sem_ctime;
};

下面简单介绍一下 semid_ds 结构中字段的含义。
sem_perm:对应于该信号量集的 ipc_perm 结构(该结构的详情请参考《System V IPC 之内存共享》)指针。
sem_base:sem 结构指针,指向信号量集中第一个信号量的 sem 结构。
sem_nsems:信号量集中信号量的个数。
sem_otime:最近一次调用 semop 函数的时间。
sem_ctime:最近一次改变该信号量集的时间。

sem 结构记录了一个信号量的信息,其声明如下:

struct sem
{
ushort semval; /* 信号量的值 */
pid_t sempid; /* 最后一次返回该信号量的进程ID 号 */
ushort semncnt; /* 等待可利用资源出现的进程数 */
ushort semzcnt; /* 等待全部资源可被独占的进程数 */
};

与信号量相关的函数

信号量集的创建与打开
要使用信号量,首先要创建一个信号量集,创建信号量集的函数声明如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h> int semget(key_t key, int nsems, int semflg);

函数 semget 用于创建一个新的信号量集或打开一个已存在的信号量集。其中参数 key 表示所创建或打开的信号量集的键。参数 nsems 表示创建的信号量集中信号量的个数,此参数只在创建一个新的信号量集时有效。参数 semflg 表示调用函数的操作类型,也可用于设置信号量集的访问权限。所以调用函数 semget 的作用由参数 key 和 semflg 决定。
当函数调用成功时,返回值为信号量的引用标识符,调用失败时,返回值为 -1。当调用 semget 函数创建一个信号量时,它相应的 semid_ds 数据结构被初始化。ipc_perm 中的各个字段被设置为相应的值,sem_nsems 被设置为 nsems 所表示的值,sem_otime 被设置为 0,sem_ctime 被设置为当前时间。

对信号量集的操作
对信号量集操作的函数声明如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h> int semop(int semid, struct sembuf *sops, size_t nsops);

参数 semid 为信号量集的引用标识符。sops 为指向 sembuf 类型的数组的指针,sembuf 结构用于指定调用 semop 函数所做的操作。sembuf 结构的定义如下:

struct sembuf
{
short sem_num; // 要操作的信号量在信号量集里的编号
short sem_op;
short sem_flag;
};

其中,sem_num 指定要操作的信号量。sem_flag 为操作标记,与此函数相关的有 IPC_NOWAIT 和 SEM_UNDO。sem_op 用于表示所要执行的操作,相应的取值和含义如下:
sem_op > 0:表示进程对资源使用完毕,交回该资源。此时信号量集的 semid_ds 结构的 sem_base.semval 将加上 sem_op 的值。若此时设置了 SEM_UNDO 位,则信号量的调整值将减去 sem_op 的绝对值。
sem_op = 0:表示进程要等待,直至 sem_base.semval 变为 0。
sem_op < 0:表示进程希望使用资源。此时将比较 sem_base.semval 和 sem_op 的绝对值大小。如果 sem_base.semval 大于等于 sem_op 的绝对值,说明资源足够分配给此进程,则 sem_base.semval 将减去 sem_op 的绝对值。若此时设置了 SEM_UNDO 位,则信号量的调整值将加上 sem_op 的绝对值。如果 sem_base.semval 小于 sem_op 的绝对值,表示资源不足。若设置了 IPC_NOWAIT 位,则函数出错返回,否则 semid_ds 结构中的 sem_base.semncnt 加 1,进程等待直至 sem_base.semval 大于等于 sem_op 的绝对值或该信号量被删除。
sops 指向的数组中的每个元素表示一个操作,由于此函数是一个原子操作,一旦执行就将执行数组中所有的操作。

信号量的控制
对信号量的具体控制操作是通过函数 semctl 来实现的,其声明如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h> int semctl(int semid, int semnum, int cmd, union semun arg);

参数 semid 为信号量集的引用标识符。参数 semnum 用于指明某个特定的信号量。参数 cmd 表示调用该函数希望执行的操作。参数 arg 是一个用户自定义的联合体:

union semun
{
int val;
struct semid_ds *buf;
ushort *array; // cmd == SETALL,或 cmd = GETALL
};

此联合中各个字段的使用情况与参数 cmd 的设置有关。具体的说明如下:
GETALL:获得 semid 所表示的信号量集中信号量的个数,并将该值存放在无符号短整型数组 array 中。
GETNCNT:获得 semid 所表示的信号量集中的等待给定信号量锁的进程数目,即 semid_ds 结构中 sem.semncnt 的值。
GETPID:获得 semid 所表示的信号量集中最后一个使用 semop 函数的进程 ID,即 semid_ds 结构中的 sem.sempid 的值。
GETVAL:获得 semid 所表示的信号量集中 semunm 所指定信号量的值。
GETZCNT:获得 semid 所表示的信号量集中的等待信号量成为 0 的进程数目,即 semid_ds 结构中的 sem.semncnt 的值。
IPC_RMID:删除该信号量。
IPC_SET:按参数 arg.buf 指向的结构中的值设置该信号量对应的 semid_ds 结构。只有有效用户 ID 和信号量的所有者 ID 或创建者 ID 相同的用户进程,以及超级用户进程可以执行这一操作。
IPC_STAT:获得该信号量的 semid_ds 结构,保存在 arg.buf 指向的缓冲区。
SETALL:以 arg.array 中的值设置 semid 所表示的信号量集中信号量的个数。
SETVAL:设置 semid 所表示的信号量集中 semnum 所指定信号量的值。

应用信号量的 demo

下面我们通过一个 demo 来看看如何在程序中使用信号量。这是一个通过共享内存进行进行进程间通信的例子:

#include <sys/types.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define SHMDATASIZE 1000
#define SN_EMPTY 0
#define SN_FULL 1
int deleteSemid = ; void server(void);
void client(int shmid, int semid);
void delete(void);
void sigdelete(int signum);
void locksem(int signum, int semnum);
void unlocksem(int semid, int semnum);
void clientwrite(int shmid, int semid, char *buffer);
union semun
{
int val;
struct semid_ds *buf;
ushort *array;
};
int safesemget(key_t key, int nssems, int semflg);
int safesemctl(int semid, int semunm, int cmd, union semun arg);
int safesemop(int semid, struct sembuf *sops, unsigned nsops);
int safeshmget(key_t key, int size, int shmflg);
void *safeshmat(int shmid, const void *shmaddr, int shmflg);
int safeshmctl(int shmid, int cmd, struct shmid_ds *buf); int main(int argc, char *argv[ ])
{
if(argc < ){
server();
}
else{
client(atoi(argv[]), atoi(argv[]));
}
return ;
} void server(void)
{
union semun sunion;
int semid, shmid;
char *buffer;
semid = safesemget(IPC_PRIVATE, , SHM_R|SHM_W);
deleteSemid = semid;
// 在服务器端程序退出时删除掉信号量集。
atexit(&delete);
signal(SIGINT, &sigdelete);
// 把第一个信号量设置为 1,第二个信号量设置为 0,
// 这样来控制:必须在客户端程序把数据写入共享内存后服务器端程序才能去读共享内存
sunion.val = ;
safesemctl(semid, SN_EMPTY, SETVAL, sunion);
sunion.val = ;
safesemctl(semid, SN_FULL, SETVAL, sunion);
shmid = safeshmget(IPC_PRIVATE, SHMDATASIZE, IPC_CREAT|SHM_R|SHM_W);
buffer = safeshmat(shmid, , );
safeshmctl(shmid, IPC_RMID, NULL);
// 打印共享内存 ID 和 信号量集 ID,客户端程序需要用它们作为参数
printf("Server is running with SHM id ** %d**\n", shmid);
printf("Server is running with SEM id ** %d**\n", semid);
while()
{
printf("Waiting until full...");
fflush(stdout);
locksem(semid, SN_FULL);
printf("done.\n");
printf("Message received: %s.\n", buffer);
unlocksem(semid, SN_EMPTY);
}
} void client(int shmid, int semid)
{
char *buffer;
buffer = safeshmat(shmid, , );
printf("Client operational: shm id is %d, sem id is %d\n", shmid, semid);
while()
{
char input[];
printf("\n\nMenu\n1.Send a message\n");
printf("2.Exit\n");
fgets(input, sizeof(input), stdin);
switch(input[])
{
case '':
clientwrite(shmid, semid, buffer);
break;
case '':
exit();
break;
}
}
}
… void locksem(int semid, int semnum)
{
struct sembuf sb;
sb.sem_num = semnum;
sb.sem_op = -;
sb.sem_flg = SEM_UNDO;
safesemop(semid, &sb, );
} void unlocksem(int semid, int semnum)
{
struct sembuf sb;
sb.sem_num = semnum;
sb.sem_op = ;
sb.sem_flg = SEM_UNDO;
safesemop(semid, &sb, );
}

由于完整的 demo 代码比较长,这里仅贴出来了程序的主干,完整的程序请访问这里
把程序代码保存到文件 sem.c 文件中,并编译:

$ gcc -Wall sem.c -o sem_demo

先不传递参数运行服务器端程序:

$ sudo ./sem_demo

然后再启动一个终端运行客户端程序,并把服务器端输出的 SHM id 和 SEM id 作为参数传入到客户端程序中:

$ sudo ./sem_demo  

服务器端(左侧窗口)程序会等待客户端(右侧窗口)程序的输入,并按照顺序把客户端中的输入在服务器端输出。服务器端程序和客户端程序通过信号量来控制对共享内存的访问,从而实现进程间数据的同步(具体的实现请参考代码)。
接着我们通过下面的命令查看系统中的 IPC 信号量:

$ ipcs -s

这就是服务器与客户端程序用来实现同步机制的信号量集!在我们的 demo 中,当服务器端程序退出时会删除掉这个信号量集,以免给系统添加垃圾。

总结

我们在《System V IPC 之共享内存》一文中写了一个很简陋的应用共享内存的 demo,由于没有应用任何的同步访问技术,其输出是比较混乱的。本文的 demo 则是在其基础上添加了信号量来控制进程对共享内存的访问。从程序的输出我们可以看到,使用信号量解决互斥共享资源的同步问题后,服务器端程序的输出变得和客户端的输入一致了。

参考:
《深入理解 Linux 内核》
《Linux 环境下 C 编程指南》

System V IPC 之信号量的更多相关文章

  1. System V IPC(2)-信号量

    一.概述                                                    System V信号量与System V消息队列不同.它不是用来在进程间传递数据.它主要 ...

  2. Linux 系统编程 学习:05-进程间通信2:System V IPC(2)

    Linux 系统编程 学习:05-进程间通信2:System V IPC(2) 背景 上一讲 进程间通信:System V IPC(1)中,我们介绍了System IPC中有关消息队列.共享内存的概念 ...

  3. 线程同步、信号量、system v IPC

    一.线程同步 条件变量 什么是条件变量? 线程A等待某个条件成立,条件成立,线程A才继续向下执行.线程B的执行使条件成立,条件成立以后唤醒线程A,以继续执行.这个条件就是条件变量. pthread_c ...

  4. 第3章 System V IPC

    3.1 概述 System V IPC 包含:System V消息队列.System V信号量.System V共享内存. 3.2 key_t 键和 ftok函数 这三种类型的System V IPC ...

  5. 《Unix网络编程》卷2 读书笔记 第3章- System V IPC

    1. 概述 三种类型的System V IPC:System V 消息队列.System V 信号量.System V 共享内存区 System V IPC在访问它们的函数和内核为它们维护的信息上共享 ...

  6. 从并发处理谈PHP进程间通信(二)System V IPC

    .container { margin-right: auto; margin-left: auto; padding-left: 15px; padding-right: 15px } .conta ...

  7. System V IPC 之共享内存

    IPC 是进程间通信(Interprocess Communication)的缩写,通常指允许用户态进程执行系列操作的一组机制: 通过信号量与其他进程进行同步 向其他进程发送消息或者从其他进程接收消息 ...

  8. System V IPC 之消息队列

    消息队列和共享内存.信号量一样,同属 System V IPC 通信机制.消息队列是一系列连续排列的消息,保存在内核中,通过消息队列的引用标识符来访问.使用消息队列的好处是对每个消息指定了特定消息类型 ...

  9. 四十九、进程间通信——System V IPC 之消息队列

    49.1 System V IPC 介绍 49.1.1 System V IPC 概述 UNIX 系统存在信号.管道和命名管道等基本进程间通讯机制 System V 引入了三种高级进程间通信机制 消息 ...

随机推荐

  1. NOIp2017 滚粗记

    NOIp2017 滚粗记 Day0 早上 早自习的时候,班主任忽然告诉我们, 我们要参加期中考试... 这对于我们真是一个沉重的打击... 但是,管不着了 明天就死去考试了 上午 \(8:10\)到了 ...

  2. [BZOJ3506] [Cqoi2014] 排序机械臂 (splay)

    Description 同OJ1552 Input Output Sample Input Sample Output HINT Source Solution Q:哎不是同一道题吗为什么分两篇博客来 ...

  3. 使用zxing生成解析二维码

    1. 前言 随着移动互联网的发展,我们经常在火车票.汽车票.快餐店.电影院.团购网站以及移动支付等各个场景下见到二维码的应用,可见二维码以经渗透到人们生活的各个方面.条码.二维码以及RFID被人们应用 ...

  4. 自言自语WEB前端面试题(一)

    刚刚得到通知,明天可能要放半天假,开心的像个200斤的傻子 我怕真是个傻子,是后天 今天的我是依旧痛经的我 于是我又来写博客了 原来,博客竟是痛经良药 接下来请看题,此题,0难度,基本不需要动脑子 J ...

  5. TP5模型关联问题

    在使用模型关联时:假如有表   merchant商户表   m_store 店铺表  m_store_ref 商户店铺关联表  user 普通用户表 $mer = Merchant::with([ ' ...

  6. 解决将龙邱oled库移植到野火工程里,oled汉字无法显示问题

    第一,检查oled是否和单片机控制引脚正确相连. GND VCC CLK:时钟信号 miso RST: DC:DATE COMMAND/CONTROL CS:CHIP SELECT 第二,检查工程里是 ...

  7. ccd采集

  8. python函数学习(一)

    1.parse_known_args()和parse_args()函数 该函数为命令行解析函数,调用时需要import argparse(命令行选项.参数和子命令的解析器). 以下内容摘自python ...

  9. ServiceFabric极简文档-1.0 Service Fabric 自定义集群部署

    Service Fabric 部署集群:https://docs.microsoft.com/zh-cn/azure/service-fabric/service-fabric-get-started ...

  10. linux修改时区

    Linux修改时区 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任 CentOS6: 查看以前的时区: [root@localhost mysq ...