Linux - 进程间通信(IPC)信号量
一、概述
进程间通信(interprocess communication,简称 IPC)指两个进程之间的通信。系统中的每一个进程都有各自的地址空间,并且相互独立、隔离,每个进程都处于自己的地址空间中。所以同一个进程的不同模块譬如不同的函数)之间进行通信都是很简单的,譬如使用全局变量等。但是,两个不同的进程之间要进行通信通常是比较难的,因为这两个进程处于不同的地址空间中。
Linux 内核提供了多种 IPC 机制,其中System V IPC 包括:System V 信号量、System V消息队列、System V 共享内存。这三种通信机制有很多相似之处。
信号量是一个计数器,与其它进程间通信方式不大相同,它主要用于控制多个进程间或一个进程内的多个线程间对共享资源的访问,相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志,除了用于共享资源的访问控制外,还可用于进程同步。
它常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源,因此,主要作为进程间以及同一个进程内不同线程之间的同步手段。Linux 提供了一组精心设计的信号量接口来对信号量进行操作,它们声明在头文件 sys/sem.h 中。
二、信号量操作接口
为了进行临界区保护。信号量一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的。
信号量是一个特殊的变量,程序对其访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。最简单的信号量是只能取0和1的变量,这也是信号量最常见的一种形式,叫做二进制信号量。而可以取多个正整数的信号量被称为通用信号量。
由于信号量只能进行两种操作等待和发送信号,即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.1、ftok函数
系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到。函数原型:
key_t ftok( const char * fname, int id )
fname:就是指定的文件名(已经存在的文件名),一般使用当前目录
id:子序号。虽然是int类型,但是只使用8bits(1-255)。
返回值:消息队列使用的ID值。
2.2、semget函数
它的作用是创建一个新信号量或取得一个已有信号量,原型为:
int semget(key_t key, int num_sems, int sem_flags);
key:信号量关联的键,由ftok函数产生。不相关的进程可以通过它访问一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget函数并提供一个键,再由系统生成一个相应的信号标识符(semget函数的返回值),只有semget函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。
num_sems:指定需要的信号量数目,它的值几乎总是1。
sem_flags:是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。
返回值:相应信号标识符(非零),失败返回-1.
2.3、semop函数
在 Linux 下,PV 操作通过调用semop函数来实现。它的作用是改变信号量的值,原型为:
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);
sem_id:是由semget返回的信号量标识符
sembuf结构的定义如下:
struct sembuf{
short sem_num;//除非使用一组信号量,否则它为0
short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,
//一个是+1,即V(发送信号)操作。
short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,
//并在进程没有释放该信号量而终止时,操作系统释放信号量
};
num_sem_ops:信号操作结构的数量,恒大于或等于1。
semop函数执行P操作时通常执行以下操作:
struct sembuf buf = { 0, -1, SEM_UNDO};
semop ( semid, &buf, 1)
semop函数执行V操作时通常执行以下操作:
struct sembuf buf = { 0, 1, SEM_UNDO};
semop ( semid, &buf, 1)
2.4、semctl函数
该函数用来控制信号量,它与共享内存的shmctl函数和消息队列的msgctl相似,它的原型为:
int semctl(int sem_id, int sem_num, int command, ...);
sem_id、sem_num两个参数与semget函数中的一样。
command:通常是下面两个值中的其中一个
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。
如果有第四个参数,它通常是一个union semum结构,定义如下:
union semun{
int val;
struct semid_ds *buf;
unsigned short *arry;
};
三、示例代码
代码演示:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
// int semget(key_t key, int nsems, int semflg);
// int semctl(int semid, int semnum, int cmd, ...);
// int semop(int semid, struct sembuf *sops, size_t nsops);
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) */
};
//P操作,拿钥匙
void PGetKey(int semid)
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = -1;
sops.sem_flg = SEM_UNDO;
semop(semid, &sops, 1);
}
//V操作,放回钥匙
void VPutBackKey(int semid)
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = 1;
sops.sem_flg = SEM_UNDO;
semop(semid, &sops, 1);
}
int main()
{
key_t key;
int semid;
if((key == ftok(".",6)) < 0)
{
printf("ftok error\n");
}
semid = semget(key , 1, IPC_CREAT|0666);//创造钥匙,数量为1
union semun sem;
sem.val = 0;//初始状态为没有钥匙
semctl(semid, 0, SETVAL, sem);//SETVAL初始化信号量为一个已知的值,这时就需要第四个参数
//0表示操作第一把钥匙
int pid = fork();
if(pid < 0)
{
printf("fork failed\n");
}
else if(pid == 0)
{
printf("this is child\n");
VPutBackKey(semid);//首先把钥匙放回
}
else
{
PGetKey(semid);//等子进程将钥匙放回后才会进行操作,保证子进程先执行
printf("this is father\n");
VPutBackKey(semid);
semctl(semid, 0, IPC_RMID);//销毁钥匙
}
return 0;
}
运行结果: