一、信号量简介
在对于临界区资源管理的过程中,多个程序同时访问一个共享资源经常容易引发一系列问题:如死锁,结果不唯一等等,使用信号量,来解决进程或线程间共享资源引发的同步问题。
让多个进程通过特殊变量展开交互,一个进程在某一个关键点上被迫停止执行直至接收到对应的特殊变量值,通过这一措施,任何复杂的进程交互要求均可得到满足,这种特殊的变量就是信号量。
特点
它是一个特殊变量,访问具有原子性。,只允许对它进行等待和发送信号这两种操作。
1)等待信号量(P)
当信号量值为0时,程序等待;当信号量值大于0时,信号量减1,程序继续运行。
2)发送信号量(V)
将信号量值加1。
工作原理
举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:
- P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
- V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1
二、信号量函数
#include <sys/sem.h>
1.semget 创建/获取信号量
int semget(key_t key, int num_sems, int sem_flags);
参数:
key是整数值(唯一非零),就是Linux线程操作中经常用到的键值,可以通过ftok函数得到,不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。
num_sems:指定需要的信号量数目
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。
sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
返回值:semget函数成功返回一个相应信号标识符(非零),失败返回-1
2.semop 修改信号量值(p v)
int semop(int semid, struct sembuf *sops, size_t numops);
该函数的作用是改变信号量的值,其实就是为了信号量的PV操作而准备的
通过判断其返回值,查看信号量集的情况
参数:
semid:信号量集的标识符
sembuf:结构体格式如下 结构体指针(参数注意&)
numops:指出将要进行操作的信号的个数
struct sembuf{
short sem_num;//信号在信号集中的索引,0代表第一个信号,1代表第二个信号
short sem_op;//操作类型: 信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg;//操作标志: 通常为SEM_UNDO,使操作系统跟踪信号,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
结构体中的sem_op 参数:
- sem_op > 0
信号加上 sem_op 的值,表示进程释放控制的资源; - sem_op = 0
如果sem_flg没有设置IPC_NOWAIT,则调用进程进入睡眠状态,直到信号量的值为0;否则进程不会睡眠,直接返回 EAGAIN - sem_op < 0
信号加上 sem_op 的值。若没有设置 IPC_NOWAIT ,则调用进程阻塞,直到资源可用;否则进程直接返回EAGAIN
结构体中的sem_flg 参数:
该参数可设置为 IPC_NOWAIT 或 SEM_UNDO 两种状态,通常为SEM_UNDO
3.semctl 初始化、删除
int semctl(int sem_id,int sem_num,int command,[union semun sem_union]);
semid :信号量集的标识符;
sem_num:即将要进行操作的信号量的编号,即信号量集合的索引值,其中第一个信号量的索引值为0。
command:将要在集合上执行的命令,通常用特定的宏代替:
常用的宏有;
- SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
eg:初始化时, semctl(semid,0,SETVAL,initsem); - IPC_RMID:从内核删除该信号量集合
eg:删除信号量 semctl(semid,0,IPC_RMID);
semun:是一个联合体
一般用到val,表示要传给信号量的初始值
union semun{
int val; //一般用到val,表示要传给信号量的初始值
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
三、demo 父子进程的信号量
父子进程的执行顺序是先父进程执行,再子进程执行。利用信号量控制,达到子进程先执行的目的
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
//联合类型semun定义
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) */
};
// V操作:
// 释放资源并将信号量值+1
// 如果有进程正在挂起等待,则唤醒它们
void vbackkey(int id)
{
struct sembuf set;
set.sem_num = 0; //信号量编号 对第一个信号量操作
set.sem_op = 1; //V操作,加一
set.sem_flg = SEM_UNDO;
semop(id,&set,1); //参数2为结构体指针 &set
printf("backkey ok\n");
}
// P操作:
// 若信号量值为1,获取资源并将信号量值-1
// 若信号量值为0,进程挂起等待
void pgetkey(int id)
{
struct sembuf set;
set.sem_num = 0;
set.sem_op = -1; //P操作,减一
set.sem_flg = SEM_UNDO;
semop(id,&set,1);
printf("getkey ok\n");
}
int main(int argc,char **argv)
{
key_t key;
key = ftok(".",2);
int semid;
// 创建一个新的信号量或者是取得一个已有信号量的键
semid = semget(key,1,IPC_CREAT|0666);
//初始化
union semun initsem;
initsem.val = 0; //将信号量值设为0!目的为了父子进程先后顺序
semctl(semid,0,SETVAL,initsem);//semtcl进行初始化
int pid = fork();
if(pid > 0)//父进程
{
//因为将信号量初始化为0,所以父进程进行P操作,父进程会被挂起等待
//待子进程进行V操作后,方可继续执行
pgetkey(semid);
printf("this is father\n");
vbackkey(semid);
//删除信号量
semctl(semid,0,IPC_RMID);
}
else if(pid == 0)//子进程
{
//父进程被挂起,子进程就会先执行printf
printf("this is child\n");
vbackkey(semid);
//子进程进行V操作,val加1,父进程就可以继续执行p
}
else
{
printf("fork error\n");
}
}
别人的代码demo
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/sem.h>//包含信号量定义的头文件
//联合类型semun定义
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
//函数声明
//函数:设置信号量的值
static int set_semvalue(void);
//函数:删除信号量
static void del_semvalue(void);
//函数:信号量P操作
static int semaphore_p(void);
//函数:信号量V操作
static int semaphore_v(void);
static int sem_id; //信号量ID
int main(int argc,char *argv[])
{
int i;
int pause_time;
char op_char = 'O';
srand( (unsigned int)getpid() );
// 创建一个新的信号量或者是取得一个已有信号量的键
sem_id = semget( (key_t)1234, 1, 0666 | IPC_CREAT );
// 如果参数数量大于1,则这个程序负责创建信号和删除信号量
if( argc > 1 )
{
if( !set_semvalue() )
{
fprintf( stderr, "failed to initialize semaphore\n" );
exit( EXIT_FAILURE );
}
op_char = 'X'; //对进程进行标记
sleep(5);
}
//循环:访问临界区
for( i = 0; i < 10; ++i )
{
// P操作,尝试进入临界区
if( !semaphore_p() )
exit( EXIT_FAILURE );
printf( "%c", op_char );
// 刷新标准输出缓冲区,把输出缓冲区里的东西打印到标准输出设备上
fflush( stdout );
pause_time = rand() % 3;
sleep( pause_time );
printf( "%c", op_char );
fflush( stdout );
// V操作,尝试离开临界区
if( !semaphore_v() )
exit( EXIT_FAILURE );
pause_time = rand() % 2;
sleep( pause_time );
}
printf( "\n %d - finished \n", getpid() );
if ( argc > 1 )
{
sleep( 10 );
del_semvalue();//删除信号量
}
return 0;
}
//函数:设置信号量的值
static int set_semvalue(void)
{
union semun sem_union;
sem_union.val = 1;
if ( semctl( sem_id, 0, SETVAL, sem_union ) )
return 0;
return 1;
}
//函数:删除信号量
static void del_semvalue(void)
{
union semun sem_union;
if( semctl( sem_id, 0, IPC_RMID, sem_union ) )
fprintf( stderr, "Failed to delete semaphore\n" );
}
//函数:信号量P操作:对信号量进行减一操作
static int semaphore_p(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0; //信号量编号
sem_b.sem_op = -1; //P操作
sem_b.sem_flg = SEM_UNDO;
if( semop( sem_id, &sem_b, 1 ) == -1 )
{
fprintf( stderr, "semaphore_p failed\n" );
return 0;
}
return 1;
}
//函数:信号量V操作:对信号量进行加一操作
static int semaphore_v(void)
{
struct sembuf sem_b;
sem_b.sem_num = 0; //信号量编号
sem_b.sem_op = 1; //V操作
sem_b.sem_flg = SEM_UNDO;
if( semop( sem_id, &sem_b, 1 ) == -1 )
{
fprintf( stderr, "semaphore_v failed\n" );
return 0;
}
return 1;
}
``