System V IPC 之信号量

时间:2022-04-03 19:29:23

本文继《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  

System V IPC 之信号量

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

$ ipcs -s

System V IPC 之信号量

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

总结

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

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

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

  1. System V IPC&lpar;2&rpar;-信号量

    一.概述                                                    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. 移动web开发之视口viewport

    × 目录 [1]布局视口 [2]视觉视口 [3]理想视口[4]meta标签[5]总结 前面的话 在CSS标准文档中,视口viewport被称为初始包含块.这个初始包含块是所有CSS百分比宽度推算的根源 ...

  2. Git--用git建立code库

    利用点时间,把自己这段时间使用git的工具的内容,使用过程中遇到的问题都梳理下.首先我们建立一个文件库(基于Ubuntu系统): 1.必须要安装: [html]  view plain copy   ...

  3. BZOJ 3992 &lbrack;SDOI 2015&rsqb; 序列统计 解题报告

    这个题最暴力的搞法就是这样的: 设 $Dp[i][j]$ 为前 $i$ 个数乘积为 $j$ 的方案数. 转移的话就不多说了哈... 当前复杂度 $O(nm^2)$ 注意到,$M$ 是个质数,就说明 $ ...

  4. Android Service 简介

    Service是Android系统中的一种组件,它跟Activity的级别差不多,但是它不能自己运行,只能后台运行,并且可以和其他组件进行交互.Service是没有界面的长生命周期的代码.Servic ...

  5. 打印zigzag矩阵

    比较愚蠢但是很好理解的一种方法 public static void printZigzag (int n){ int[][] arr = new int[n][]; //动态创建数组 并初始化 fo ...

  6. spring-boot学习资料

    spring-boot: http://www.ityouknow.com/spring-boot 这里的内容都可以学习下: https://zhuanlan.zhihu.com/dreawer?to ...

  7. 深度学习篇——Tensorflow-GPU配置

    tensoflow-gpu安装 对于python 3.5和3.6的童鞋们而言,安装tensorflow其实并不难,因为我们可以通过pip直接安装. 不过,第一要求你安装的python是64位的,如下图 ...

  8. RMQ求LCA

    题目链接 rmq求LCA,interesting. 一直没有学这玩意儿是因为CTSC的Day1T2,当时我打的树剖LCA 65分,gxb打的rmq LCA 45分... 不过rmq理论复杂度还是小一点 ...

  9. tp3&period;2 支付宝手机网站支付

    手机网站支付接口,支付宝官方文档:https://b.alipay.com/signing/productSet.htm?navKey=all 第一步: 1)登陆支付宝企业账号 进入支付宝官网 ,登陆 ...

  10. WinForm 之 应用程序开机自启动设置方法

    一.原理 需要开机自启动的程序,需要将其启动程序的路径写到注册表中指定的文件夹下. 二.实现方式 方法1:在生成安装程序时配置: 方法2:在程序运行时动态配置. 三.在生成安装程序时配置 1.右击安装 ...