信号量是是什么?
信号量是进程间通信方式之一,用来实现进程间的同步与互斥。信号量的原理是一种数据操作锁的概念,它本身不具备数据交换的功能,而是通过控制其他通信资源(如文本、外部设备等)来实现进程间通信。信号量本身不具备数据传输的功能,他只是一种外部资源的标识。
信号量的本质是:具有等待队列的计数器。
相关概念介绍:
临界资源:多个进程或线程可以共享的资源。
临界区:对临界资源进行操作的代码。
同步:访问临界资源的时序可控性。
互斥:同一时间访问临界资源的唯一性。
为了获得共享资源,进程需要执行以下步骤:
1.测试控制该资源的信号量
2.若此信号为正,当进程对信号量做减1操作时,表明它使用了一个单位的资源。
3.如此信号量的值为0,则进程进入休眠状态,直至信号量值大于0时。进程被唤醒后再执行第1步。
4.当进程不再使用该信号量控制的共享资源时,该信号值增加1.如果此时有等待此信号量的进程,则唤醒它。
注意:常用的信号量形式被称为二元信号量或双态信号量。它控制单个资源,初值为1。其实信号量初值可以为任一正值,该值说明有多少个共享资源单位可供使用。信号量是对临界资源的一种保护机制,所以对它操作是不能被打断的即原子性,而这些都是内核帮我们做好的。
信号量的操作步骤:
int semget(key_t key, int nsems, int semflg);
参数:
key:操作系统对该信号量的标识。
nsems:创建该信号量集合中的信号量数。
semflg:IPC_CREAT、IPC_EXCL等(创建标记)。
返回值:成功返回信号量操作句柄semid;失败返回-1,并设置errno。
int semctl(int semid, int semnum, int cmd, ...);
参数:
semid:信号量ID
semnum:只要操作的第几个信号量。
cmd:为具体所要进行的操作,常用的操作:SETVAL设置单个信号量的初值。
SETALL:设置所有信号量的初值,此时的semnum会被忽略。
IPC_RMID:删除信号量。
…:不定参数,可有可无。如:我们需要获取信号量信息时就可通过这个参数获取。
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) */
};
当我们要控制信号量的时候,必须手动定义这个联合体。
val:是对信号量所要赋的初值。
buf:关于信号量的一些详细信息存放在buf中
array:信号量集,用来设置信号量集中的信号量或获取。
int semop(int semid, struct sembuf *sops, unsigned nsops);
参数:
semid:所要操作信号量集的ID。
sops:对该信号量集的操作(下面详细说明sembuf)。
nsops:要操作的信号数量。
返回值:成功返回0;失败返回-1,并设置errno。
struct sembuf:这个结构体在我们PV操作时会用到。
struct sembuf
{
unsigned short int sem_num; /* semaphore number */
short int sem_op; /* semaphore operation */
short int sem_flg; /* operation flag */
};
数据成员:
sem_num:所要操作第几个信号量
sem_op:所要做的操作,赋值为-1表示P操作(资源申请),赋值为1表示V操作(资源释放)。
short_flag:通常设置为SEM_UNDO,使得操作系统能够跟踪信号量,并在没有释放信号量时自动释放。
下面我们可以通过一个小案例来实现一个同步互斥的例子:
案例1:加入现在我们有一个人是专门生产糖果的,还有一个人是专门吃糖果的,那么这两个人就必须遵守一个时序(吃糖果的人必须要在生糖果人生产出糖果后才能吃到糖果),这用专业术语就叫对同一资源访问的时序性控制。程序说明:父进程(生产糖果的人)、子进程(吃糖果的人)
sync.c(同步):
#include
#include
#include
#include
#define KEY 0x01234567
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
//PV操作
void sem_p(int semid)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = -1;
buf.sem_flg = SEM_UNDO;
semop(semid,&buf,1);
}
void sem_v(int semid)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = SEM_UNDO;
semop(semid,&buf,1);
}
int main()
{
//1.创建1个信号量
int semid = semget(KEY,1,IPC_CREAT|0664);
if(semid<0)
{
perror("semget error!\n");
return 1;
}
//同过父子进程完成同步操作
if(fork()==0)
{
while(1)
{
sem_p(semid);
printf("吃了一颗糖!!\n");
sem_v(semid);
}
}
else
{ //2.设置信号量初值,需要定义一个union semun
union semun sem;
sem.val = 1;
if(semctl(semid,0,SETVAL,sem)<0)
{
perror("semctl error!");
return 2;
}
while(1)
{
sem_p(semid);
printf("生产了一颗糖!!\n");
sleep(1);
sem_v(semid);
}
}
//3.最后要释放信号量
semctl(semid,0,IPC_RMID);
return 0;
}
效果展示:
案例2:父子进程争抢显示器资源父进程打印B然后睡上1毫秒再打印个B、子进程打印A然后睡上1毫秒再打印个A。案例分析:如果我们不对显示器资源进行保护,那么可能在父子进程休眠的过程中,双方都对显示器资源操作,此时打印出来的将是 父子进程数据的交错信息。
print.c:
#include
#include
int main()
{
pid_t pid = -1;
if((pid = fork())<0)
{
perror("fork!");
return 1;
}
if(pid == 0)
{
while(1)
{
printf("A");
fflush(stdout);
usleep(1000);
printf("A ");
fflush(stdout);
}
}
else if(pid>0)
{
while(1)
{
printf("B");
fflush(stdout);
usleep(1000);
printf("B ");
fflush(stdout);
}
//3.最后要释放信号量
semctl(semid,0,IPC_RMID);
return 0;
}
}
此时我们看到父子进程交叉进行,而没有做到(同一时刻对共享资源的唯一访问性即就是互斥)。
**下面通过互斥来保护同一时刻只能有一个进程访问显示器。
#include
#include
#include
#include
#define KEY 0x01234567
union semun
{
int val;
struct semid_ds *buf;
unsigned short *array;
};
//PV操作
void sem_p(int semid)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = -1;
buf.sem_flg = SEM_UNDO;
semop(semid,&buf,1);
}
void sem_v(int semid)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = SEM_UNDO;
semop(semid,&buf,1);
}
int main()
{
//1.创建1个信号量
int semid = semget(KEY,1,IPC_CREAT|0664);
if(semid<0)
{
perror("semget error!\n");
return 1;
}
//2.设置信号量初值,需要定义一个union semun
union semun sem;
sem.val = 1;
if(semctl(semid,0,SETVAL,sem)<0)
{
perror("semctl error!");
return 2;
}
//同过父子进程完成同步操作
if(fork()==0)
{
while(1)
{
sem_p(semid);
printf("A");
fflush(stdout);
usleep(1000);
printf("A ");
fflush(stdout);
sem_v(semid);
}
}
else
{
while(1)
{
sem_p(semid);
printf("B");
fflush(stdout);
usleep(1000);
printf("B ");
fflush(stdout);
sem_v(semid);
}
//3.最后要释放信号量
semctl(semid,0,IPC_RMID);
return 0;
}
}
运行效果展示:
此时我们看到A、B是成双成对出现的说明同一个时刻父子进程只有一个在操作显示器(输出设备)。
通过对信号量的学习,我们就可以对上次共享内存的server&client案例进行改进了,这里就不在演示了,后面有机会在更新,这里关于信号量重点已经讲完。