Linux-System V信号量

时间:2024-04-25 16:22:36

目录

  • System V信号量
    • 创建或打开信号量
    • 操作信号量
    • 信号量撤销值
    • 控制信号量
      • IPC_RMID
      • IPC_STAT
      • IPC_SET
      • GETVAL
      • SETVAL
      • GETPID
      • GETNCNT
      • GETZCNT
    • 代码示例

System V信号量

信号量的作用和消息队列不太一样,消息队列的作用是进程之间传递消息。而信号量的作用是为了同步多个进程的操作。

它支持两种原子操作,wait和signal。wait还可以称为down、P或lock,signal还可以称为up、V、unlock或post。其作用分别是原子地增加和减少信号量的值。

一般来说,信号量是和某种预先定义的资源相关联的。信号量元素的值,表示与之关联的资源的个数。内核会负责维护信号量的值,并确保其值不小于0。

信号量上支持的操作有:

  • 将信号量的值设置成一个绝对值。
  • 在信号量当前值的基础上加上一个数量。
  • 在信号量当前值的基础上减去一个数量。
  • 等待信号量的值等于0。在上述操作中,后两个可能会陷入阻塞。在第三种情况中,当信号量的当前值小于要减去的值时,操作会陷入阻塞。当信号量的值不小于要减去的值时,内核会唤醒阻塞进程。在第四种情况中,如果当前信号量的值不为0,该操作会陷入阻塞,直到信号量的值变为0为止。
信号量操作 语义
将信号量的值设置成一个绝对值 初始化资源的个数为某绝对值
在信号量当前值的基础上加上一个数量N 释放N个资源
在信号量当前值的基础上减去一个数量N 申请N个资源,可能因资源不够而陷入阻塞
等待信号量的值等于0 等待可用资源个数变为0,可能会陷入阻塞

使用最广泛的信号量是二值信号量(binary semaphore)。对于这种信号量而言,它只有两种合法值:0和1,对应一个可用的资源。若当前有资源可用,则与之对应的二值信号量的值为1;若资源已被占用,则与之对应的二值信号量的值为0。当进程申请资源时,如果当前信号量的值为0,那么进程会陷入阻塞,直到有其他进程释放资源,将信号量的值加1才能被唤醒。

从这个角度看,二值信号量和互斥量所起的作用非常类似。那信号量和互斥量有何不同之处呢?

互斥量(mutex)是用来保护临界区的,所谓临界区,是指同一时间只能容许一个进程进入。而信号量(semaphore)是用来管理资源的,资源的个数不一定是1,可能同时存在多个一模一样的资源,因此容许多个进程同时使用资源。

信号量是互斥量的一个扩展,由于资源数目增多,增强了并行度。

和二值信号量相比,System V信号量在两个维度上都做了扩展。

第一,资源的数目可以是多个。资源个数超过1个的信号量称为计数信号量(counting semaphore)。

第二,允许同时管理多种资源,由多个计数信号量组成的一个集合称为计数信号量集,每个计数信号量管理一种资源。比如第一种资源的总数是5,第二种资源的总数是10。在使用过程中可选择申请哪种资源或哪几种资源。

创建或打开信号量

创建或打开信号量的函数为semget,其接口定义如下:

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

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

第一个参数和消息队列一样,可以随机选择一个整数值,也可以通过ftok函数创建。

可以参考https://blog.****.net/m0_51415606/article/details/138001330?spm=1001.2014.3001.5502

第二个参数nsems表示信号量集中信号量的个数。换句话说,就是要控制几种资源。大部分情况下只控制一种。如果并非创建信号量,仅仅是访问已经存在的信号量集,可以将nsems指定为0。

第三个参数semflg支持多种标志位。目前支持IPC_CREAT和IPC_EXCL标志位,和消息队列一样。

在创建信号量时,需要考虑的问题是系统限制。系统的限制可以分成三个层面。

  • 系统容许的信号量集的上限:SEMMNI
  • 单个信号量集中信号量的上限:SEMMSL
  • 系统容许的信号量的上限:SEMMNS
限制 说明 最大值
SEMMNI 系统容许创建的信号量集上限 32768(IPCMNI)
SEMMSL 一个信号量集里信号量的最大数量 65536
SEMMNS 系统中所有信号量集里的信号量总数的上限 2147483647(INT_MAX)

操作信号量

semop函数负责修改集合中一个或多个信号量的值,其定义如下:

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

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

第一个参数是通过semget获取到的信号量的标识符ID。

第二个参数是sembuf类型的指针。sembuf结构体定义在sys/sem.h头文件中。一般来说,该结构体至少包含以下三个成员变量:

struct sembuf {
    unsigned short int sem_num ;
    short sem_op ;
    short sem_flg;
}

成员变量sem_num解决的是操作哪个信号量的问题。因为信号量集中可能存在多个信号量,需要用这个参数来告知semop函数要操作的是哪个信号量,0表示第一个信号量,1表示第二个信号量,依此类推,最大为nsems-1,即不得超过集合中信号量的个数。如果sem_num的值小于0,或者大于等于集合中信号量的个数,semop调用则会返回失败,并置errno为EFBIG。

一般来讲,不建议采用如下方法来初始化sembuf:

struct sembuf  myopsbuf = {1,-1,0}

因为考虑到可移植性,我们并没有十足的把握可以确定sembuf结构体中成员变量的顺序和上面定义中给出的顺序是严格一致的。(不过Linux的定义就是上面给出的定义,若不考虑可移植性,可以放心采用上面的方法。)

semop函数的典型用法如下所示:

struct sembuf myopsbuf[3] ;
myopsbuf[0].sem_num = 0;   /*操作信号量集中的第0个信号量*/
myopsbuf[0].sem_op = -1;   /*信号量0的值减去1,即申请1个资源*/
myopsbuf[0].sem_flg = 0 ;
myopsbuf[1].sem_num = 1;   /*操作信号量集中的第1个信号量*/
myopsbuf[1].sem_op = 2 ;   /*信号量1的值加上2*/
myopsbuf[1].sem_flg = 0;
myopsbuf[2].sem_num = 2;   /*操作信号量集中的第2个信号量*/
myopsbuf[2].sem_op = 0;    /*等待第2个信号量的值变为0*/
myopsbuf[2].sem_flg = 0;
if(semop(semid,myopsbuf,3) == -1)
{
    /*error handler here*/
}

semop函数每次会操作一组信号量,每个信号量由一个sembuf来表示,修改一个信号量最好也将其定义成struct sembuf ops[1]这样的数组,semop函数的第三个参数表示要操作的信号量的个数。

如果调用semop函数同时操作多个信号量,要被原子地执行,要么内核完成所有操作,要么内核什么也不做。

信号量撤销值

使用信号量存在这样一种风险,即进程申请了资源,修改了信号量的值,却没来得及释放资源就异常退出了。异常退出的进程把资源带进了坟墓,而其他进程却在苦苦等待其释放资源。这就意味着资源泄漏,即该进程申请的资源再也无法给其他进程使用了。对于二值信号量来说,资源泄漏的危害尤其大。

为了避免因这个问题而陷入不可收拾的境地,内核提供了一种解决方案,即内核会负责记住进程对信号量施加的影响,当进程退出的时候,内核负责撤销该进程对信号量施加的影响。

调用semop函数时,可以通过如下方法设置SEM_UNDO标志位。

struct sembuf  myopsbuf[1];
myopsbuf[0].sem_num = 0;
myopsbuf[0].sem_op = -1;  /*信号量0的值减去1*/
myopsbuf[0].sem_flg |= SEM_UNDO ;
semop(semid,myopsbuf,1);

控制信号量

信号量控制相关的结构体:

struct ipc_perm{
    key_t key;
    uid_t uid;
    gid_t gid;
    uid_t cuid;
    gid_t cgid;
    mode_t mode;
     ulong_t seq ;
};

/*信号量控制相关的结构体*/
struct semid_ds {
     struct ipc_perm sem_perm;
     ...
};

控制信号量的函数为semctl函数,其定义如下:

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

某些特定的操作需要第四个参数,第四个参数是联合体,很不幸的是这个联合体需要程序员自己定义,代码如下所示:

union semun {
   int              val;
   struct semid_ds *buf;
   unsigned short  *array;
   struct seminfo  *__buf;  /*Linux特有的*/
};

根据第三个参数cmd值的不同,semctl支持以下命令:

IPC_RMID

semctl函数的第二个参数被忽略。和消息队列的删除一样,内核不会维护信号量集的引用计数,说删就删,而且是立即删除信号量集。所有阻塞在semop函数上的进程将被唤醒,返回错误并置errno为ERMID。

删除信号量的示例代码如下:

int semaphore_destroy(int semid)
{
    union semun ignored_argument;
    semctl(semid, 0, IPC_RMID,ignored_argument);
}

IPC_STAT

用于获取信号量集的信息,并存放在union semun中buf指向的结构体。

每个信号量集都有一个与之关联的semid_ds结构体(该结构体无须自己定义),它至少包含以下成员:

struct ipc_perm sem_perm;
time_t sem_otime;
time_t sem_ctime;
unsigned long sem_nsems;

可以使用如下的简单代码来获取上述信息(省略错误处理):

struct semid_ds ds ;
union semun arg;   /*须确保semun联合体已经定义*/
arg.buf = &ds ;
semctl(semid,0,IPC_STAT,arg);
printf(“last op time is %s\n”,ctime(&(ds.sem_otime)));

IPC_SET

union semun arg的成员变量buf,可用来设置sem_perm.uid、sem_perm.gid和sem_perm.mode。

GETVAL

返回集合中第semnum个信号量的值,无需第四个参数,示例代码如下:

int semaphore_getval(int semid,int index)
{
    union semun ignored_argument;
    return semctl(semid, index, GETVAL, ignored_argument);
}

SETVAL

将信号量集中的第semnum个信号的值设置为arg.val,示例代码如下:

int semaphore_setval(int semid, int index, int value)
{
    union semun arg;
    arg.val = value;return semctl(semid, index, SETVAL,arg);
}

GETPID

返回上一个对第semnum个信号量执行semop的进程的进程ID,如果不存在,则返回0。

GETNCNT

返回等待第semnum个信号量值增大的进程的个数。

GETZCNT

返回等待第semnum个信号量值变成0的进程的个数。

代码示例

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

#define SEM_KEY 1234
#define NUM_SEMS 1
#define NUM_PROCESSES 5 // 进程池中进程的数量

void process_task() {
    // 模拟任务处理
    printf("Task processed by process %d\n", getpid());
    sleep(3); // 模拟任务处理时间
}

int main()
{
    int sem_id;
    struct sembuf sem_op[1];
    int i;

    // 创建一个信号量集合,其中包含一个信号量
    sem_id = semget(SEM_KEY, NUM_SEMS, IPC_CREAT | IPC_EXCL);
    if (sem_id == -1)
    {
        perror("semget");
        exit(EXIT_FAILURE);
    }

    // 初始化信号量的值为进程池中进程的数量,表示有多少个进程可以同时执行任务
    if (semctl(sem_id, 0, SETVAL, 1) == -1)
    {
        perror("semctl");
        exit(EXIT_FAILURE);
    }

    // 创建进程池
    for (i = 0; i < NUM_PROCESSES; i++)
    {
        pid_t pid = fork();
        if (pid == -1)
        {
            perror("fork");
            exit(EXIT_FAILURE);
        }
        else if (pid == 0)
        {
            // 子进程
            while (1)
            {
                // 获取信号量
                sem_op[0].sem_num = 0;
                sem_op[0].sem_op = -1; // 减少信号量的值
                sem_op[0].sem_flg = SEM_UNDO;

                if (semop(sem_id, sem_op, 1) == -1) {
                    perror("semop");
                    exit(EXIT_FAILURE);
                }

                // 处理任务
                process_task();

                // 释放信号量
                sem_op[0].sem_op = 1; // 增加信号量的值

                if (semop(sem_id, sem_op, 1) == -1)
                {
                    perror("semop");
                    exit(EXIT_FAILURE);
                }
            }
        }
    }

    // 等待子进程结束
    for (i = 0; i < NUM_PROCESSES; i++)
        wait(NULL);

    // 删除信号量
    if (semctl(sem_id, 0, IPC_RMID) == -1)
    {
        perror("semctl");
        exit(EXIT_FAILURE);
    }

    return 0;
}

[root@Zhn 信号量]# ./test
Task processed by process 2397
Task processed by process 2398
Task processed by process 2395
Task processed by process 2396
Task processed by process 2399
Task processed by process 2397
Task processed by process 2398
Task processed by process 2395
^C
[root@Zhn 信号量]# 

每隔三秒打印一次,五个进程轮询打印。