System V信号量

1. System V IPC

概述

以下三种类型的IPC合称为System V IPC:

  • System V信号量
  • System V消息队列
  • System V共享内存

System V IPC在访问它们的函数和内核为它们维护的信息上有一些类似点,主要包括:

  • IPC键和ftok函数
  • ipc_perm结构
  • 创建或打开时指定的用户访问权限
  • ipcs和ipcrm命令

下表汇总了所有System V IPC函数。

信号量 消息队列 共享内存
头文件 sys/sem.h sys/msg.h sys/shm.h
创建或打开IPC的函数 semget msgget shmget
控制IPC操作的函数 semctl msgctl shmctl
IPC操作函数 semop msgsnd
msgrcv
shmat
shmdt

IPC键和ftok函数

三种类型的System V IPC都使用IPC键作为它们的标识,IPC键是一个key_t类型的整数,该类型在 sys/types.h 中定义。

IPC键通常是由 ftok 函数赋予的,该函数把一个已存在的路径名pathname和一个非0整数id组合转换成一个key_t值,即IPC键。

#include <sys/ipc.h>

//成功返回IPC键,失败返回-1
key_t ftok(const char *pathname, int id);

参数说明:

  • pathname在是程序运行期间必须稳定存在,不能反复创建与删除
  • id不能为0,可以是正数或者负数

ipc_perm结构

内核给每个IPC对象维护一个信息结构,即 struct ipc_perm 结构,该结构及System V IPC函数经常使用的常值定义在 sys/ipc.h 头文件中。

struct ipc_perm
{
    uid_t   uid;   //owner's user id
    gid_t   gid;   //owner's group id
    uid_t   cuid;  //creator's group id
    gid_t   cgid;  //creator's group id
    mode_t  mode;  //read-write permissions
    ulong_t seq;   //slot usage sequence number
    key_t   key;   //IPC key
};

创建与打开IPC对象

创建或打开一个IPC对象使用相应的xxxget函数,它们都有两个共同的参数:

  • 参数key,key_t类型的IPC键
  • 参数oflag,用于指定IPC对象的读写权限(ipc_perm.mode),并选择是创建一个新的IPC对象还是打开一个已存在的IPC对象

对于参数key,应用程序有两种选择:

  • 调用ftok,给它传pathname和id
  • 指定key为 IPC_PRIVATE ,这将保证会创建一个新的、唯一的IPC对象,但该标志不能用于打开已存在的IPC对象,只能是新建

对于参数oflag,如上所述,它包含读写权限、创建或打开这两方面信息:

IPC_CREAT

ipcs和ipcrm命令

  • 由于System V IPC的三种类型不是以文件系统路径名标识的,因此无法使用ls和rm命令查看与删除它们
  • ipcs和ipcrm分别用于查看与删除系统中的System V IPC
usage : ipcs -asmq -tclup 
    ipcs [-s -m -q] -i id
    ipcs -h for help.
usage: ipcrm [ [-q msqid] [-m shmid] [-s semid]
          [-Q msgkey] [-M shmkey] [-S semkey] ... ]

2. System V信号量

计数信号量集

我们已经知道了Posix信号量采用计数信号量,System V信号量在此基础上增加了一级复杂度,它采用计数信号量集,计数信号量集是由一个或多个计数信号量构成的集合。

对于系统中的每个信号量集,内核都维护一个 struct semid_ds 信息结构,它定义在 sys/sem.h 头文件中。

struct semid_ds
{
    struct ipc_perm  sem_perm;
    struct sem       *sem_base;  //指向信号量集的指针
    ushort           sem_nsems;  //信号量集中的信号量个数
    time_t           sem_otime;  //上一次调用semop的时间
    time_t           sem_ctime;  //创建时间或上一次以IPC_SET调用semctl的时间
};

其中,sem_base是指向信号量集的指针,信号量集中的每个成员都对应一个struct sem结构:

struct sem
{
    ushort_t  semval;  //信号量的值
    short     sempid;  //上一次成功调用semop,或以SETVAL、SETALL调用semctl的进程ID
    ushort_t  semncnt; //等待semval变为大于当前值的线程数
    ushort_t  semzcnt; //等待semval变为0的线程数
};

semget

semget用于创建一个新的信号量集或打开一个已存在的信号量集。

  • nsems参数指定集合中的信号量个数,如果是打开一个已存在的信号量集,就把该参数设为0
  • oflag参数可设置为IPC_CREAT,以及SEM_R和SEM_A指定的访问权限,如果是打开一个已存在的信号量集,就把该参数设为0
  • 成功时返回一个称为信号量标识符的非负整数,semop和semctrl函数将使用它
//成功返回信号量标识符,失败返回-1
int semget(key_t key, int nsems, int oflag);

当实际操作为创建一个新的信号量集时,相应semid_ds结构中与集合中每个信号量关联的struct sem结构并不初始化,而是在以SETVAL或SETALL命令调用semctrl时初始化的。

也就是说,创建一个新的System V信号量集(semget)并将它初始化(semctl)需要两次函数调用,

这是System V信号量的一个致命缺陷。

semop

使用semget打开一个信号量集后,对其中一个或多个信号量值的操作就使用semop函数。

//成功返回0,失败返回-1
int semop(int semid, struct sembuf *ops, size_t nops);
  • semid指定待操作的信号量集
  • nops为集合中的信号量个数
  • ops指向一个struct sembuf类型的结构数组,该数组中的每个元素给目标信号量集中某个特定的信号量指定sem_op操作,这个特定的信号量由sem_num指定

我们只保证sembuf含有以下三个成员,不保证这三个成员的顺序,也不保证还有其他成员,因此sembuf数组元素必须采用如ops[0].sem_num = 0所示的方法进行初始化。

struct sembuf
{
    unsigned short sem_num;  /* semaphore number */
    short          sem_op;   /* semaphore operation */
    short          sem_flg;  /* operation flags */
};

sem_op指定的操作有三类:

  • sem_op > 0:操作内容为semval += sem_op,这对应于释放某个信号量控制的资源
  • sem_op = 0:调用者阻塞等待直到semval变为0
  • sem_op < 0:调用者阻塞等待直到semval >= abs(sem_op),调用者阻塞等待直到semval>这对应于分配资源

sem_flg可设置的值有:0、IPC_NOWAIT、SEM_UNDO,一般使用0,对于其余两个值:

  • IPC_NOWAIT用于给信号量集中某个特定信号量设置非阻塞标志
  • SEM_UNDO为System V信号量特有的复旧机制

semctl

semctl函数对一个信号量集执行各种控制操作。

//成功根据cmd返回相应的非负值,失败返回-1
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);
  • semid指定待控制的信号量集
  • semnum指定信号量集内的某个成员, 仅在cmd为GETVAL、SETVAL、GETNCNT、GETZCNT和GETPID时使用
  • cmd指定控制命令
  • arg是可选的,取决于cmd的具体值

第四个参数arg是可选的,取决于cmd的值, 当需要用到该参数时,应用程序必须按如下结构定义union semun:

union semun
{
   int              val;    /* Value for SETVAL */
   struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
   unsigned short  *array;  /* Array for GETALL, SETALL */
   struct seminfo  *__buf;  /* Buffer for IPC_INFO (Linux-specific) */
};

定义如下术语用于后续cmd说明:

  • semval:信号量的当前值
  • sempid:上一次成功调用semop,或以SETVAL、SETALL调用semctl的进程ID
  • semncnt:等待semval变为大于当前值的线程数
  • semzcnt:等待semval变为0的线程数

System V支持下列cmd值,除非特殊说明,否则成功时返回值均为0。

cmd 说 明
GETVAL 把semval的当前值作为函数返回值返回,它是一个unsigned short类型的整数
SETVAL 把semval的值设为arg.val
GETPID 把sempid的当前值作为函数返回值返回
GETNCNT 把semncnt的当前值作为函数返回值返回
GETZCNT 把semzcnt的当前值作为函数返回值返回
GETALL 返回信号量集内每个成员的semval值,这些值通过arg.array返回,arg.array需由调用者分配内存
SETALL 设置信号量集内每个成员的semval值,这些值通过arg.array指定, 此时第二个参数semnum设为0即可
IPC_STAT 返回信号量集当前的semid_ds结构,该结构通过arg.buf返回,arg.buf需由调用者分配内存, 可以用该命令获得信号量集中的信号量个数
IPC_SET 设置信号量集对应semid_ds结构中的sem_perm.uid、sem_perm.gid和sem_perm.mode,设置的值来自arg.buf
IPC_RMID 删除由semid指定的信号量集, 此时第二个参数semnum设为0即可

其中,前五个命令针对的都是信号量集semid中由semnum指定的信号量。

3. 测试程序

代码实现

semcreate.c

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

#define FTOK_FILE       "/home/delphi/ftok.file"
#define FTOK_ID         1
#define SEM_MODE        0666

/*
#define SEM_MODE_OWNER  SEM_R | SEM_A
#define SEM_MODE_GROUP  (SEM_R >> 3) | (SEM_A >> 3)
#define SEM_MODE_OTHER  (SEM_R >> 6) | (SEM_A >> 6)
#define SEM_MODE        (SEM_MODE_OWNER | SEM_MODE_GROUP | SEM_MODE_OTHER)
*/

int main()
{
    int nsems = 3;
    int oflag = IPC_CREAT | SEM_MODE;
    key_t key = ftok(FTOK_FILE, FTOK_ID);
    int semid = semget(key, nsems, oflag);

    if (semid >= 0)
    {
        printf("semcreate success, semid = %d\n", semid);
    }

    return 0;
}

这里遇到个问题,SEM_MODE一开始是按注释部分定义的,但man semget给出的三个头文件都已经包含了,编译时却报错,提示SEM_R和SEM_A未定义,不知道为什么,只能用0666定义了。

semrmid.c

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

#define FTOK_FILE       "/home/delphi/ftok.file"
#define FTOK_ID         1

int main()
{
    key_t key = ftok(FTOK_FILE, FTOK_ID);
    int semid = semget(key, 0, 0);
    semctl(semid, 0, IPC_RMID);

    return 0;
}

semsetvalues.c

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

#define FTOK_FILE       "/home/delphi/ftok.file"
#define FTOK_ID         1

union semun
{
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO (Linux-specific) */
};

int main(int argc, char **argv)
{
    key_t key;
    int semid;
    int nsems;
    unsigned short *semvals;
    union semun arg;
    struct semid_ds seminfo;
    int i;

    /* 打开semcreate创建的信号量集 */
    key = ftok(FTOK_FILE, FTOK_ID);
    semid = semget(key, 0, 0);

    /* 获得信号量集中的信号量个数 */
    arg.buf = &seminfo;
    semctl(semid, 0, IPC_STAT, arg);
    nsems = arg.buf->sem_nsems;

    /* 设置信号量集中每个信号量的值 */
    semvals = (unsigned short *)calloc(nsems, sizeof(unsigned short));

    for (i = 0; i < nsems; i++)
    {
        semvals[i] = atoi(argv[i + 1]); //通过命令行参数分别指定集合中每个信号量的值
    }

    arg.array = semvals;
    semctl(semid, 0, SETALL, arg);

    return 0;
}

semgetvalues.c

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

#define FTOK_FILE       "/home/delphi/ftok.file"
#define FTOK_ID         1

union semun
{
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO (Linux-specific) */
};

int main(int argc, char **argv)
{
    key_t key;
    int semid;
    int nsems;
    unsigned short *semvals;
    union semun arg;
    struct semid_ds seminfo;
    int i;

    /* 打开semcreate创建的信号量集 */
    key = ftok(FTOK_FILE, FTOK_ID);
    semid = semget(key, 0, 0);

    /* 获得信号量集中的信号量个数 */
    arg.buf = &seminfo;
    semctl(semid, 0, IPC_STAT, arg);
    nsems = arg.buf->sem_nsems;

    /* 获得信号量集中每个信号量的值 */
    semvals = (unsigned short *)calloc(nsems, sizeof(unsigned short));
    arg.array = semvals;
    semctl(semid, 0, GETALL, arg);

    for (i = 0; i < nsems; i++)
    {
        printf("semvals[%d] = %d\n", i, semvals[i]);
    }

    return 0;
}

semops.c

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

#define FTOK_FILE       "/home/delphi/ftok.file"
#define FTOK_ID         1

union semun
{
    int              val;    /* Value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO (Linux-specific) */
};

int main(int argc, char **argv)
{
    key_t key;
    int semid;
    int nsems;
    union semun arg;
    struct semid_ds seminfo;
    struct sembuf *semops;
    int i;

    /* 打开semcreate创建的信号量集 */
    key = ftok(FTOK_FILE, FTOK_ID);
    semid = semget(key, 0, 0);

    /* 获得信号量集中的信号量个数 */
    arg.buf = &seminfo;
    semctl(semid, 0, IPC_STAT, arg);
    nsems = arg.buf->sem_nsems;

    /* 对信号量集中的所有信号量进行相同的semop操作 */
    semops = (struct sembuf *)calloc(nsems, sizeof(struct sembuf));

    for (i = 0; i < nsems; i++)
    {
        semops[i].sem_num = i;
        semops[i].sem_op = atoi(argv[1]); //操作类型由命令行参数指定,>0, 0, <0
        semops[i].sem_flg = 0;
    }

    semop(semid, semops, nsems);

    return 0;
}

运行测试

  • 运行semcreate,通过运行前后的ipcs -s,可以确认信号量集创建成功

  • 运行semsetvalues,将三个信号量的值分别设为1、2、3
  • 然后运行semgetvalues,打印信息和我们设置的值一致,符合预期

  • 运行semops,并指定sem_op > 0
  • 然后运行semgetvalues,打印信息显示每个信号量的值都增加了1,符合预期

  • 运行semrmid,通过运行前后的ipcs -s,可以确认信号量集已经从系统删除

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章