信号量的值与相应资源的使用情况有关,当它的值大于 0 时,表示当前可用的资源数的数量;当它的值小于 0 时,其绝对值表示等待使用该资源的进程个数。信号量的值仅能由 PV 操作来改变。它本质是一种数据操作锁,本身不具有数据交换的功能,而是通过控制其他的通信资源来实现进程间通信,它本身只是一种外部资源的标识,信号量在此过程中负责数据操作的互斥,同步等功能。
我们对信号量的值进行的增减操作均为原子操作,我们使用信号量的目的就是为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题。而信号量就可以让一个临界区同一时间只有一个线程在访问它,也就是说,信号量是用来协调进程对共享资源访问的。
在 Linux 下,PV 操作通过调用semop函数来实现。该函数定义在头文件 sys/sem.h中,原型如下:
int semop(int semid,struct sembuf *sops,size_t nsops);
函数的参数 semid 为信号量集的标识符;参数 sops 指向进行操作的结构体数组的首地址;参数 nsops 指出将要进行操作的信号的个数。semop 函数调用成功返回 0,失败返回 -1。
semop 的第二个参数 sops 指向的结构体数组中,每个 sembuf 结构体对应一个特定信号的操作。因此对信号量进行操作必须熟悉该数据结构,该结构定义在 linux/sem.h,如下所示:
struct sembuf{
unsigned short sem_num; //信号在信号集中的索引,0代表第一个信号,1代表第二个信号
short sem_op; //操作类型
short sem_flg; //操作标志
};
对信号量最基本的操作就是进行PV操作,而System V信号量正是通过 semop 函数和 sembuf 结构体的数据结构来进行PV操作的。
当 sembuf 的第二个数据结构 sem_op 设置为负数时,是对它进行P操作,即减1操作;当设置为正数时,就是进行V操作,即加1操作。
下面我们通过一段代码来看看信号量的应用:
我们通过编写信号量的创建,销毁,以及PV操作函数来实现一个简单的信号量:
comm.h文件
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define _PATH_ "."
#define _PROJ_ID_ 0x6666
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*/
};
int creat_sem_set(int _sem_nums);
int get_sem_set(int _sem_nums);
int init_sem_set(int _sem_id,int _sem_nums,int _init_val);
int p_sem(int _sem_id,int _seq_num);
int v_sem(int _sem_id,int _seq_num);
int destroy_sem_set(int _sem_id);
comm.c文件
#include "comm.h"
static int comm_creat_sem_set(int _sem_nums,int flag)
{
key_t _key=ftok(_PATH_,_PROJ_ID_);
if(_key<0)
{
perror("ftok");
return -1;
}
int sem_id=semget(_key,_sem_nums,flag);
if(sem_id<0)
{
return -1;
}
return sem_id;
}
int creat_sem_set(int _sem_nums)
{
umask(0);
int flag=IPC_CREAT|IPC_EXCL|0666;
return comm_creat_sem_set(_sem_nums,flag);
}
int get_sem_set(int _sem_nums)
{
int flag=IPC_CREAT;
return comm_creat_sem_set(_sem_nums,flag);
}
int init_sem_set(int _sem_id,int _sem_num,int _init_val)
{
union semun _un;
_un.val=_init_val;
if(semctl(_sem_id,_sem_num,SETVAL,_un)<0)
{
perror("semctl");
return -1;
}
return 0;
}
static int comm_op_sem(int _sem_id,int _seq_num,int _op)
{
struct sembuf _sem_buf[1];
_sem_buf[0].sem_num=_seq_num;
_sem_buf[0].sem_op=_op;
_sem_buf[0].sem_flg=0;
if(semop(_sem_id,_sem_buf,1)<0)
{
perror("semop");
return -1;
}
return 0;
}
int p_sem(int _sem_id,int _seq_num)
{
return comm_op_sem(_sem_id,_seq_num,-1);
}
int v_sem(int _sem_id,int _seq_num)
{
return comm_op_sem(_sem_id,_seq_num,1);
}
int destroy_sem_set(int _sem_id)
{
if(semctl(_sem_id,0,IPC_RMID,NULL)<0)
{
perror("semctl");
return -1;
}
return 0;
}
下面我们写一个小的测试程序,我们fork()一个进程,然后父子进程分别向标准输出端输出信息,子进程输出A,父进程输出B,我们会看到如下图所示的现象:
sem.c文件
#include "comm.h"
int main()
{
int sem_id=creat_sem_set(1);
pid_t id=fork();
if(id<0)
{
perror("fork");
return -1;
}
else if (id==0)
{
while(1)
{
printf("A");
sleep(1);
fflush(stdout);
printf("A");
sleep(3);
fflush(stdout);
}
}
else
{
while(1)
{
printf("B");
sleep(1);
fflush(stdout);
printf("B");
sleep(2);
fflush(stdout);
}
waitpid(id,NULL,0);
}
destroy_sem_set(sem_id);
return 0;
}
结果如下:
结果分析:我们会看到父子进程是在交叉打印的,完全没有规律,证明父子进程之间是在争夺临界资源的,谁抢到了谁就执行,所以出现混乱情况。
而我们用上信号量之后,代码如下:
int main()
{
int sem_id=creat_sem_set(1);
init_sem_set(sem_id,0,1);
pid_t id=fork();
if(id <0)
{
perror("fork");
exit(1);
}
else if(id==0)
{
int sem_id=get_sem_set(0);
while(1)
{
p_sem(sem_id,0);
printf("A");
sleep(1);
fflush(stdout);
printf("A");
sleep(2);
fflush(stdout);
v_sem(sem_id,0);
}
}
else
{
while(1)
{
p_sem(sem_id,0);
printf("B");
sleep(1);
fflush(stdout);
printf("B");
sleep(2);
fflush(stdout);
v_sem(sem_id,0);
}
}
destroy_sem_set(sem_id);
return 0;
}
结果如下:
结果分析:我们可以看到,不是先执行子进程就是先执行父进程,实现了进程间临界资源的互斥访问。