一、信号量的定义和原理

1、一些概念

  • 原子操作不可中断的一个或者一系列的操作,即一件事要么做要么不做
  • 临界资源:不同进程能够看到的一份公共资源,一次只能被一个进程使用
  • PV操作:由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
  • P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

2、信号量的定义

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的

3、信号量的原理

1. 测试控制该资源的信号量;

2. 若信号量的值为正,则进程可以使用该资源,进程的信号量值减1,表示一个资源被使用;

3. 若此信号量为0,则进程进入休眠,直到该信号量值大于0

4. 当进程不再使用一个由一个信号控制的共享资源时,该信号量加1,如果有进程正在休眠等待该信号量,则该进程会被唤醒


二、信号量的使用

1、一些数据结构的定义

(1)semid_ds

内核为每个信号量集合维护着一个结构体:

struct semid_ds{
	struct ipc_perm sem_perm;
	unsigned short sem_nsems;
	time_t sem_otime;
	time_t sem_ctime;
	...
}

(2)semun(必须定义该联合体)

union semun{
	int 			val;
	struct semid_ds *buf;
	unsigned short  *array;
}

(3)sembuf(信号量操作数组)

struct sembuf{
	// 除非使用一组信号量,否则它为0,一般从0,1,...num_secs-1
	unsigned short	sem_num; 
	// 信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,
	// 即P(等待)操作,一个是+1,即V(发送信号)操作。
	short 			sem_op;
	// 通常为SEM_UNDO,使操作系统跟踪信号,
	// 并在进程没有释放该信号量而终止时,操作系统释放信号量。
	short  			sem_flg;
}

2、semget函数

#include <sys/sem.h>

int semget(key_t key, int num_sems, int sem_flags);
  • 功能创建一个新信号量集或取得一个已有信号量集
  • 参数
  • key:整数值(唯一非零),不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作;
  • num_sems:指定需要的信号量数目,它的值几乎总是1;
  • sem_flags:一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
  • 返回值:成功返回一个相应信号标识符(非零),失败返回-1。

3、semop函数

#include <sys/sem.h>

int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
  • 功能它的作用是改变信号量的值
  • 参数
  • sem_id:由semget返回的信号量标识符;
  • sem_opa:表示一个由sembuf结构表示的信号量操作数组;
  • num_sem_ops:规定该数组中操作的数量。
  • 返回值:成功返回0,失败返回-1。

4、semctl函数

#include <sys/sem.h>

int semctl(int sem_id, int sem_num, int command, ...);
  • 功能直接控制信号量的信息
  • 参数
  • sem_id:信号量标识符;
  • sem_num:信号量的值;
  • command:通常是下面两个值中的其中一个:
  • SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置;
  • IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
  • 返回值:成功返回0,失败返回-1。

三、信号量的demo

// comm.h
#ifndef _MYSEM_H_
#define _MYSEM_H_

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

#define PATHNAME "."  // ftok 中生成key值 . 表示当前路径
#define PROJ_ID  56  // ftok 中配合PATHNAME 生成唯一key值

union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *_buf;
};

int create_sems(int nums);  // 创建含有nums个信号量的集合
int get_sems();     // 获取信号量
// 初始化semid对应的信号量集中编号为which的信号量值为value
int init_sems(int semid , int which, int value);
int destroy_sems(int semid); // 释放该信号量集
int P(int semid, int which);    // 表示分配 信号量值-1
int V(int semid, int which);    // 表示释放 信号量值+1

#endif /* _MYSEM_H_ */
// comm.cpp
#include "comm.h"

// 创建信号量和获取信号量公用函数
static int comm_sem(int nums , int semflag)
{
    // ftok 函数把一个已存在的路径名和一个整数标识转换成一个key_t值,
    // 即IPC关键字。
    key_t key = ftok(PATHNAME, PROJ_ID);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    // 用来创建一个信号集,或者获取已存在的信号集。
    int semid = semget(key, nums, semflag); 
    if( semid < 0)
    {
        perror("semget");
        return -1;
    }
    return semid;
}

// 初始化操作数组
static int comm_sem_op(int semid, int which, int op)
{
    struct sembuf _sembuf;
    _sembuf.sem_num = which;
    _sembuf.sem_op = op;
    _sembuf.sem_flg = 0; //  IPC_NOWAIT  SEM_UNDO
    return semop(semid, &_sembuf, 1);
}

// 创建含有nums个信号量的集合
int create_sems(int nums)
{
    return comm_sem(nums, IPC_CREAT|IPC_EXCL|0666);
}

// 初始化信号集
int init_sems(int semid , int which, int value)
{
    union semun _semun;
    _semun.val = value;
    int ret = semctl(semid, which, SETVAL,_semun);
    if(ret < 0)
    {
        perror("inin_sem");
        return -1;
    }
    return 0;
}

// 获取信号量
int get_sems()    
{
    return comm_sem(0, IPC_CREAT);
}

// 释放该信号量集
int destroy_sems(int semid) 
{
    int ret = semctl(semid, 0, IPC_RMID, NULL);
    if(ret < 0)
    {
        perror("rm_sem");
        return -1;
    }
    return 0;
}

// P操作
int P(int semid, int which)    
{
    return comm_sem_op(semid, which , -1);
}

// V操作
int V(int semid, int which)    
{
    return comm_sem_op(semid, which, 1);
}
// test.cpp
#include "comm.h"
#include <stdio.h>
#include <unistd.h>

int main()
{
    int semid = create_sems(10); // 创建一个包含10个信号量的信号集
    init_sems(semid, 0, 1);  // 初始化编号为 0 的信号量值为1
    pid_t id = fork(); // 创建子进程
    if (id < 0)
    {
        perror("fork");
        return -1;
    }
    else if (0 == id) // 子进程
    { 
        int sem_id = get_sems();
        while(1)
        {
            P(sem_id, 0); // 对该信号量集中的0号信号做P操作
            printf("你");
            fflush(stdout);
            sleep(1);
            printf("好");
            printf(":");
            fflush(stdout);
            sleep(1);
            V(sem_id, 0);
        }
    }
    else // 父进程
    {
        while(1)
        {
            P(semid,0);
            printf("在");
            sleep(1);
            printf("吗");
            printf("?");
            fflush(stdout);
            V(semid, 0);
        }
        wait(NULL);
    }
    destroy_sems(semid);
    return 0;
}

四、信号量的总结

信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。我们经常通过信号来解决多个进程对同一资源的访问竞争的问题,使在任一时刻只能有一个执行线程访问代码的临界区域,也可以说它是协调进程间的对同一资源的访问权,也就是用于同步进程的

【优点】:

  • 可以同步进程。

【缺点】:

  • 信号量有限。