本文继《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. [BZOJ3460] Jc的宿舍

    bzoj 题面放一下 Description WC2014后无数人来膜拜jc,但是来膜拜的人实在太多了, 而且很多人是一连膜拜好几天.所以jc给这些人建了一座树 形的宿舍,而根节点(1号节点)住着jc ...

  2. [APIO2009]抢掠计划

    题面: Description Siruseri城中的道路都是单向的.不同的道路由路口连接.按照法律的规定,在每个路口都设立了一个Siruseri银行的ATM取款机.令人奇怪的是,Siruseri的酒 ...

  3. webpack3中使用postcss-loader和autoprefixer给css3样式添加浏览器兼容

    1.在webpack中需要处理css必须先安装如下两个loader npm install --save-dev css-loader style-loader 2.要处理sass和添加浏览器前缀需要 ...

  4. IT连创业系列:新的一年,先淫文一篇!

    办公室窗外,有鸟声〜〜 在IT连创业走过的日子里,这是我第一次听见鸟声. 也许,是曾经的忙碌,封锁了自己的心眼. 岁月秒秒: 当初燃烧的火焰,从红,烧成了蓝. 曾经的内心湃澎,化成了平淡坚持. 但这, ...

  5. Hadoop3.0完全分布式集群安装部署

    1. 配置为1个namenode(master主机),2个datanode(slave1主机+slave2主机)的hadoop集群模式, 在VMWare中构建3台运行Ubuntu的机器作为服务器: 关 ...

  6. Hadoop搭建全程

    修改配置文件 cd /etc/sysconfig/network-scripts  #进入网络配置目录 dir ifcfg*                         #找到网卡配置文件 ifc ...

  7. gulp菜鸟级零基础详细教程

    : 相信大家一定听说过gulp或者webpack,grunt等前端构建工具.gulp是前端开发过程中对代码进行构建的工具,是自动化项目的构建利器:她不仅能对网站资源进行优化,而且在开发过程中很多重复的 ...

  8. css实现div中图片高度自适应并与父级div宽度一致

    需求:1.父级div不设置高度 2.图片高度自适应,并且显示为正方形: 以前遇到列表中图片高度必须和父级宽度相同,并且需要为正方形的时候,最开始的方法是定死图片高度,这样会导致不同分辨率下图片会压缩, ...

  9. PAT-L2-007-gplt真题

    题目分析: 1. 首先,题目说一个家庭有孩子爸爸妈妈等几辈人,可以利用并查集将一个家庭里的所有人变成一个集合: 2. 刚好题目的目的也是这样,输出的是一个家庭人数,人均房产面积,人均房产套数等: 3. ...

  10. 网络通信 --> 互联网协议(一)

    互联网协议 一.概述 如何分层有不同的模型,有的模型分七层,有的分四层.这里介绍把互联网分成五层. 最底下的一层叫做"实体层"(Physical Layer),最上面的一层叫做&q ...