Linux/Unix 编程 —— System V信号量

目录

1 介绍

2 信号量各种操作合集

1. 创建信号量

2. 初始化信号量集

3. 信号量控制

4. 关联结构

5 信号量操作

6. 信号量删除

3 二元信号量协议实现(可重用)


1 介绍

信号量提供一种访问机制,让一个临界区同一时间只有一个线程在访问它, 也就是说信号量是用来调协进程对共享资源的访问的,防止出现因多个程序同时访问一个共享资源而引发的一系列问题。其中共享内存的使用就要用到信号量。

一个信号量是一个由内核维护的整数,其值被限制为大于或等于0。在-一个信 号量上可以执行各种操作( 即系统调用),包括:

  • 将信号量设置成-一个绝对值;
  • 在信号量当前值的基础上加上一个数量;
  • 在信号量当前值的基础上减去一个数量;
  • 等待信号量的值等于0。

Linux/Unix 编程 —— System V信号量_第1张图片

 

使用SystemV信号量的常规步骤如下。

  • 使用 semget()创建或打开一个信号量集。
  • 使用semct()SETVAL或SETALL操作初始化集合中的信号量。(只有一个进程需要完成这个任务。)
  • 使用semop()操作信号量值。使用信号量的进程通常会使用这些操作来表示一种共享资源的获取和释放。
  • 当所有进程都不再需要使用信号量集之后使用semctl() IPC_RMID操作删除这个集合。(只有一个进程需要完成这个任务。)

2 信号量各种操作合集

1. 创建信号量

#include 
#include 
#include 

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

key参数是使用45.2节中描述的其中一种方法生成的键(通常使用值IPC_PRIVATE或由ftok()返回的键)。

如果使用semget()创建一个新信号量集,那么nsems会指定集合中信号量的数量,并且其值必须大于0。如果使用semget()来获取一个既有集的标识符,那么nsems必须要小于或等于集合的大小(否则会发生EINVAL错误)。无法修改一个既有集中的信号量数量。

semflg参数是一个位掩码,它指定了施加于新信号量集之上的权限或需检查的一个既有集合的权限。指定权限的方式与为文件指定权限的方式是一样的(表15-4)。此外,在semflg中可以通过对下列标记中的零个或多个取OR来控制semget()的操作。

IPC_CREAT

如果不存在与指定的key相关联的信号量集,那么就创建一个新集合。

IPC_EXCL

如果同时指定了IPC_CREAT并且与指定的key关联的信号量集已经存在,那么返回EEXIST错误。

semget()在成功时会返回新信号量集或既有信号量集的标识符。后续引用单个信号量的系统调用必须要同时指定信号量集标识符和信号量在集合中的序号。一个集合中的信号量从0开始计数。

 

2. 初始化信号量集

根据SUSv3的要求,实现无需对由semget()创建的集合中的信号量值进行初始化。相反,程序员必须要使用semctl()系统调用显式地初始化信号量。(在Linux上,semget()返回的信号量实际上会被初始化为0,但为取得移植性就不能依赖于此。)前面曾经提及过,信号量的创建和初始化必须要通过单独的系统调用而不是单个原子步骤来完成的事实可能会导致在初始化一个信号量时出现竞争条件。

union semun arg;
struct sembuf sop;
arg.val = 0;
/* So initialize it to 0 */
if (semctl(semid, 0, SETVAL, arg) == -1)
    errExit(" semctl" );

3. 信号量控制

#include 
#include 
#include 

int semctl(int semid, int semnum, int cmd, ...);

semid参数是操作所施加的信号量集的标识符。对于那些在单个信号量上执行的操作,semnum参数标识出了集合中的具体信号量。对于其他操作则会忽略这个参数,并且可以将其设置为0。cmd参数指定了需执行的操作。

一些特定的操作需要向semctl()传入第四个参数,在本节余下的部分中将这个参数命名为arg。这个参数是一个union semun, 程序清单47-2给出了其定义。在程序中必须要显式地定义这个union。

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

常规控制操作

下面的操作与可应用于其他类型的System V IPC对象上的操作是一样的。 所有这些操作都会忽略semnum参数。45.3 节提供了有关这些操作的更多细节,包括调用进程所需的特权和权限。

IPC_RMID

立即删除信号量集及其关联的semid ds数据结构。所有因在semop()调用中等待这个集合中的信号量而阻塞的进程都会立即被唤醒,semop()会报告错误EIDRM。这个操作无需arg参数。

IPC_STAT

在arg.buf指向的缓冲器中放置一份与这个信号量集相关联的semid_ds数据结构的副本。

IPC_SET

使用arg.buf指向的缓冲器中的值来更新与这个信号量集相关联的semid_ds数据结构中选中的字段。

获取和初始化信号量值

下面的操作可以获取或初始化一个集合中的单个或所有信号量的值。获取一个信号量的值需具备在信号量上的读权限,而初始化该值则需要修改(写)权限。

GETVAL

semctl()返回由semid指定的信号量集中第semnum个信号量的值。这个操作无需arg参数。

SETVAL

将由semid指定的信号量集中第semnum个信号量的值初始化为arg.val。

GETALL

获取由semid指向的信号量集中所有信号量的值并将它们放在arg.array 指向的数组中。程序员必须要确保该数组具备足够的空间。(通过由IPC_STAT操作返回的semid_ds 数据结构中的sem_nsems 字段可以获取集合中的信号量数量。)这个操作将忽略semnum参数。

SETALL

使用arg.array 指向的数组中的值初始化semid指向的集合中的所有信号量。这个操作将忽略semnum参数。

获取单个信号量的信息

下面的操作返回(通过函数结果值) semid引用的集合中第semnum个信号量的信息。所有这些操作都需要在信号量集合中具备读权限,并且无需arg参数。

GETPID

返回上一个在该信号量上执行semop()的进程的进程ID;这个值被称为sempid 值。如果还没有进程在该信号量上执行过semop(),那么就返回0。

GETNCNT

返回当前等待该信号量的值增长的进程数;这个值被称为semnent值。

GETZCNT

返回当前等待该信号量的值变成0的进程数;这个值被称为semzcnt值。

4. 关联结构

struct semid_ds {
               struct ipc_perm sem_perm;  /* Ownership and permissions
               time_t          sem_otime; /* Last semop time */
               time_t          sem_ctime; /* Last change time */
               unsigned short  sem_nsems; /* No. of semaphores in set */
       };      
struct ipc_perm {
       key_t key;            /* Key supplied to semget() */
       uid_t uid;            /* Effective UID of owner */
       gid_t gid;            /* Effective GID of owner */
       uid_t cuid;           /* Effective UID of creator */
       gid_t cgid;           /* Effective GID of creator */
       unsigned short mode;  /* Permissions */
       unsigned short seq;   /* Sequence number */
   };

sem_perm

在创建信号量集时按照45.3中所描述的那样初始化这个子结构中的字段。通过IPC_SET能够更新uid、gid 以及mode子字段。

sem_otime

在创建信号量集时会将这个字段设置为0,然后在每次成功的semop()调用或当信号量值因SEM_UNDO操作而发生变更时将这个字段设置为当前时间(参见47.8 节)。这个字段和sem_ctime的类型为time_t,它们存储自新纪元到现在的秒数。

sem_ctime

在创建信号量时以及每个成功的IPC_ SET、SETALL和SETVAL操作执行完毕之后将这个字段设置为当前时间。

sem_ nsems

在创建集合时将这个字段的值初始化为集合中信号量的数量。

5 信号量操作

#include 
#include 
#include 

int semop(int semid, struct sembuf *sops, unsigned nsops);

int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);

sops参数是一个指向数组的指针, 数组中包含了需要执行的操作,nsops 参数给出了数组的大小(数组至少需包含一个元素)。操作将会按照在数组中的顺序以原子的方式被执行。sops数组中的元素是形式如下的结构。

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

sem_num字段标识出了在集合中的哪个信号量上执行操作。sem_op 字段指定了需执行的操作。

  • 如果sem_op大于0。那么就将sem_op的值加到信号量值上,其结果是其他等待减小信号量值的进程可能会被唤醒并执行它们的操作。调用进程必须要具备在信号量上的修改(写)权限。
  • 如果sem_op等于0,那么就对信号量值进行检查以确定它当前是否等于0。如果等于0,那么操作将立即结束,否则semop()就会阻塞直到信号量值变成0为止。调用进程必须要具备在信号量上的读权限。
  • 如果sem_op小于0,那么就将信号量值减去sem_op。 如果信号量的当前值大于或等于sem_op的绝对值,那么操作会立即结束。否则semop()会阻塞直到信号量值增长到在执行操作之后不会导致出现负值的情况为止。调用进程必须要具备在信号量上的修改权限。

当semop()调用阻塞时,进程会保持阻塞直到发生下列某种情况为止。

  • 另一个进程修改了信号量值使得待执行的操作能够继续向前。
  • 一个信号中断了semop调用。发生这种情况时会返回EINTR错误。(在21.5节中指出过semop在被一个信号处理器中断之后是不会自动重启的。)
  • 另一个进程删除了semid引用的信号量。发生这种情况时semop会返回EIDRM错误。

semtimedop中的timeout参数是一个指向timespec 结构(参见23.4.2节)的指针,通过这个结构能够将一个时间间隔表示为秒数和纳秒数。如果在信号量操作完成之前所等待的时间已经超过了规定的时间间隔,那么semtimedop会返回EAGAIN错误。如果将timeout指定为NULL,那么semtimedop就与semop完全一样了。

6. 信号量删除

semctl(semid, semnum, IPC_RMID)

3 二元信号量协议实现(可重用)

头文件

/* binary_sems.h

   Header file for binary_sems.c.
*/
#ifndef BINARY_SEMS_H           /* Prevent accidental double inclusion */
#define BINARY_SEMS_H

#include "tlpi_hdr.h"

/* Variables controlling operation of functions below */

extern Boolean bsUseSemUndo;            /* Use SEM_UNDO during semop()? */
extern Boolean bsRetryOnEintr;          /* Retry if semop() interrupted by
                                           signal handler? */

int initSemAvailable(int semId, int semNum);

int initSemInUse(int semId, int semNum);

int reserveSem(int semId, int semNum);

int releaseSem(int semId, int semNum);

#endif

源文件

/* binary_sems.c

   Implement a binary semaphore protocol using System V semaphores.
*/
#include 
#include 
#include "semun.h"                      /* Definition of semun union */
#include "binary_sems.h"

Boolean bsUseSemUndo = FALSE;
Boolean bsRetryOnEintr = TRUE;

int                     /* Initialize semaphore to 1 (i.e., "available") */
initSemAvailable(int semId, int semNum)
{
    union semun arg;

    arg.val = 1;
    return semctl(semId, semNum, SETVAL, arg);
}

int                     /* Initialize semaphore to 0 (i.e., "in use") */
initSemInUse(int semId, int semNum)
{
    union semun arg;

    arg.val = 0;
    return semctl(semId, semNum, SETVAL, arg);
}

/* Reserve semaphore (blocking), return 0 on success, or -1 with 'errno'
   set to EINTR if operation was interrupted by a signal handler */

int                     /* Reserve semaphore - decrement it by 1 */
reserveSem(int semId, int semNum)
{
    struct sembuf sops;

    sops.sem_num = semNum;
    sops.sem_op = -1;
    sops.sem_flg = bsUseSemUndo ? SEM_UNDO : 0;

    while (semop(semId, &sops, 1) == -1)
        if (errno != EINTR || !bsRetryOnEintr)
            return -1;

    return 0;
}

int                     /* Release semaphore - increment it by 1 */
releaseSem(int semId, int semNum)
{
    struct sembuf sops;

    sops.sem_num = semNum;
    sops.sem_op = 1;
    sops.sem_flg = bsUseSemUndo ? SEM_UNDO : 0;

    return semop(semId, &sops, 1);
}

 

 

你可能感兴趣的:(Linux)