信号量

信号量(Semaphore)是一种用于实现计算机资源共享的IPC机制之一,其本质是一个计数器。信号量是在多进程环境下实现资源互斥访问或共享资源访问的方法,可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,进程/线程必须获取一个信号量;一旦该关键代码段完成了,那么该进程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个进程释放信号量。

信号量有两种应用形式:一种用于临界资源的互斥访问,临界资源在同一时刻只允许一个进程使用,此时的信号量是一个二元信号量,它只控制一个资源;另一种应用于处理多个共享资源(例如多台打印机的分配),信号量在其中起到记录空闲资源数目的作用。

Linux信号量定义

Linux多进程访问共享资源时,需要按下列步骤进行操作:

(1)检测控制这个资源的信号量的值。

(2)如果信号量是正数,就可以使用这个资源。进程将信号量的值减一,表示当前进程占用了一份资源。

(3)如果信号量是0,那么进程进入睡眠状态,直到信号量的值重新大于0时被唤醒,转入第一步操作。

上述过程也被称为PV操作。为了正确实现信号量机制,检测和增减信号量的值都应该是原语操作,因此信号量一般是在内核中实现的。

在信号量的实际应用中,是不能单独定义一个信号量的,只能定义一个信号量集,其中包含一组信号量。同一信号量集中的信号量使用同一个引用ID,这样的设置是为了多个资源或同步操作的需要。每个信号量集都有一个与之对应的结构,其中记录了信号量集的各种信息,该结构的定义如下:

#include <sys/sem.h> 
  
struct semid_ds 
{ 
    struct ipc_perm sem_perm;    //指向与信号量集相对应的ipc_perm结构的指针 
    struct sem *sem_base;        //指向这个集合中第一个信号量的指针 
    ushort sem_nsems;            //集合中信号量的数量 
    time_t sem_otime;            //最近一次调用semop函数的时间 
    time_t sem_ctime;            //最近一次改变的时间 
};

semid_ds结构中的sem结构记录了单一信号量的一些信息:

struct sem 
{ 
    ushort semval;    //信号量的值 
    pid_t sempid;     //最近一次执行操作的进程的进程号 
    ushort semncnt;   //等待信号值增长,即等待可利用资源出现的进程数 
    ushort semzcnt;   //等待信号值减少,即等待全部资源可被独占的进程数 
};

信号量操作

创建或打开

semget函数用于创建或打开一个信号量集,其中的参数key和参数semflg的取值和用法与消息队列的msgget函数相同。nsems参数用于指出信号量集中创建的信号量数量,如果是打开一个已存在的信号量集,该参数被忽略。如果调用成功则返回信号量集标识符,否则返回-1。

#include <sys/sem.h> 
#include <sys/ipc.h> 
#include <sys/types.h> 
  
int semget(key_t key, int nsems, int semflg);

当一个新的信号量集被创建时,与之相关联的semid_ds被初始化:

·ipc_perm被初始化,其中mode的设置会按照semflg的要求进行。

·em_otime被置为0。

·sem_ctime被设置为当前时间。

·sem_nsems被置为参数nsems的值。

操作信号量集

semop函数用于对信号量集进行操作。

#include <sys/ipc.h> 
#include <sys/sem.h> 
#include <sys/types.h> 
  
int semop(int semid, struct sembuf *sops, unsigned nsops);

参数semid是一个通过semget函数返回的信号量集标识符;参数nsops标明了参数sops所指向数组中的元素个数;参数sops为sembuf结构的数组,其中每个元素表示一个操作,semop是原子操作,因此一旦函数执行就将执行数组中的全部操作。

结构sembuf用来说明semop函数要对信号量集执行的操作:

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

sembuf结构中,sem_num是相对应的信号量集中的某一个资源(即指定将要操作的信号量),所以其值是一个从0到响应的信号量集的资源总数(ipc_perm.sem_nsems)之间的整数。sem_op指明要执行的操作,sem_flg说明semop的行为,其值及所对应的操作如下:

·sem_op>0:表示进程对资源使用完毕后释放的资源数量,并将sem_op的值加到信号量的值上。

·sem_op = 0:进程阻塞直到信号量的相应值为0,当信号量已经为0,函数立即返回。如果信号量的值不为0,则依据sem_flg的IPC_NOWAIT位决定函数动作。sem_flg指定IPC_NOWAIT,则semop函数出错返回,若没有指定则将该信号量的semncnt加1,然后进程挂起直到下述情况发生:信号量的值为0,将信号量的semzcnt的值减1,函数semop成功返回;此信号量被删除(只有超级用户或创建用户进程才有此权限),函数semop出错返回;进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt减1,函数semop出错返回。

·sem_op<0:请求sem_op的绝对值资源。如果相应的资源数可以满足请求,则将该信号量减去sem_op的绝对值,函数成功返回。当相应的资源数不能满足请求时,则这个操作与sem_flg有关。若sem_flg指定IPC_NOWAIT,则semop函数出错返回,若没有指定则将该信号量的semncnt加1,然后进程挂起直到下述情况发生:当相应的资源数可以满足请求,则该信号的值减去sem_op的绝对值,函数成功返回;此信号量被删除,函数semop出错返回;进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt减1,函数semop出错返回。

以下代码是使用semop函数的示例:

#include <iostream> 
#include <cstdlib> 
#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/sem.h> 
  
int main(int argc, char *argv[]) 
{ 
    //创建信号量集 
    int nsems = 1; //设置信号量集的信号量数量为1 
    int sem_id = semget(IPC_PRIVATE, nsems, 0666); 
    if (sem_id < 0) 
    { 
        std::cerr << "创建信号量集失败\n"; 
        exit(1); 
    } 
    std::cout << "成功创建信号量集,标识符为 " << sem_id << "\n"; 
  
    //定义信号量集操作 
    struct sem_buf buf; 
    buf.sem_num = 0; //设置当前可用资源数为0 
    buf.sem_op = 1; //释放资源数 
    buf.sem_flg = IPC_NOWAIT; 
  
    //对信号量集进行释放资源操作 
    if (semop(sem_id, &buf, nsems) < 0) 
    { 
        std::cerr << "semop\n"; 
        exit(1); 
    } 
  
    //查看系统IPC状态 
    system("ipcs -s"); 
  
    //删除信号量集 
    if (semctl(sem_id, 0, IPC_RMID) < 0) 
    { 
        std::cerr << "删除信号量集失败\n"; 
        exit(1); 
    } 
  
    exit(0); 
}

控制信号量集

semctl是信号量集控制函数,用于控制信号量集,如设置单个信号量的值、删除信号量集等。其标准调用格式如下:

#include <sys/types.h> 
#include <sys/ipc.h> 
#include <sys/sem.h> 
  
int semctl(int semid, int semmum, int cmd, ...);

参数semid是信号量集的标识符,参数semmum用于指定信号量集中的某一个信号量(类似于数组下标,从0开始),参数cmd则用于指定具体的操作。

cmd参数说明
参数值 说明
SETVAL 设置单个信号量的值
GETALL 返回信号量集中所有信号量的值
SETALL 设置信号量集中所有信号量的值
IPC_STAT 放置与信号量集相连的semid_ds结构当前值于arg.buf指定的缓冲区
IPC_SET 用arg.buf指定结构值替代与信号量集合相连的semid_ds结构的值
GETVAL 返回单个信号量的值
GETPID 返回最后一个操作该信号量集的进程ID
GETNCNT 返回semncnt的值
GETZCNT

返回semzcnt的值

IPC_RMID 删除指定的信号量集

 执行IPC_SET、IPC_RMID命令的进程只能是信号量集的创建进程、拥有者进程或特权进程,执行其他命令的进程必须拥有信号量集的读取或更新权限。

根据实际cmd参数的具体内容,semctl可能拥有第4个参数arg,这是一个共用体(或称联合体),其中的val用于semctl中cmd值为SETVAL时,指明要设置的信号量值;buf用于IPC_STAT/IPC_SET,表示存放信号量集合数据结构的缓冲区;array用于GETALL/SETALL,存放所获得的或要设置的信号量集中所有信号量的值。结构定义如下:

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

如果semctl函数调用成功,则返回值大于等于0(当semctl操作为GET操作时返回相应的值,其余返回0);如果调用失败则返回-1,并设置错误变量errno为对应的值。 


发布评论
IT序号网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

MinGW-w64安装教程——著名C/C++编译器GCC的Windows版本知识解答
你是第一个吃螃蟹的人
发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。