IPC是进程间的通信(interprocess communication)的简称。传统上该术语描述的是运行在某个操作系统上的不同的进程间各种消息传递(message passing)。
IPC分为两代,初代System V IPC比较全面,现在用的Posix IPC更加易懂。不管是哪代IPC,重点都是这3点:
1. 信号量
2. 共享内存
3. 消息队列
Linux下命令行输入 ipcs 可查看信息量,共享内存和消息队列。
—,信号量
信号量是一个特殊的整数值,主要是用来控制多个进程对临界资源的互斥访问,进程根据信号量来判断是否有可供利用的资源。信号量主要是用来实现进程同步机制。
信号量是一个计数器,可用于同步多进程对共享数据对象的访问,为了获得共享资源,进程需要执行以下操作:
1,测试控制该资源的信号量操作后的值。
2,若此操作后的信号量值大于等于0,则进程可以使用该资源,进程将信号量值减1,表示它使用了一个资源单位。
3,若此操作后的信号量为小于0的值,则表示没有资源可用,该进程将被阻塞,当信号量大于0时,进程才会得到资源继续运行。
当进程不再使用由一个信号量控制得共享资源时,该信号量值增1。如果有进程正在睡眠以等待此信号量,则唤醒他们,为了正确地实现信号量,信号量值得测试及减1操作应当是原子操作,为此,信号量通常是在内核中实现得。
常用的信号量一般初始值为1,只控制单个资源,有时也称互斥锁,但是,信号量得初值可以是任意一正值,该值说明有多少个共享资源单位可供共享应用,信号量有以下3个特性:
1、信号量并非是一个非负值,而必须将信号量定义为含有一个或多个信号量值的集合,当创建一个信号量时,要指定该集合中的各个值。(linux/sem.h struct semid_ds)
sreuct sem
{
ushort_t semvl;
short sempid;
ushort semncnt;
ushort semzcnt;
};
2、创建信号量对其赋初值分开,这是一个致命弱点,因为不能原子地创建一个信号量集合,并且对该集合中的所有值赋初值。
3、即使没有进程使用,但他们仍然存在,因此必须考虑在进程终止时有没有释放得信号量。
以上的三个特性就导致了信号使用的复杂性。
Linux中,信号量的主要操作函数主要有以下:
key_t ftok(char *pathname, char proj);
根据参数pathname和proj 来创建一个关键字,成功时返回与路径pathname相对应的一个键值,具有唯一性,失败时返回值为-1。
int semget(key_t key, int nsems, int semflg);
创建一个新信号量或者取得一个现有的信号量,key是一个关键字,可以是用ftok()函数创建的,也可以是IPC_PRIVATE(/usr/include/bits$ vi ipc.h),nsems表明创建的信号量个数,semflg是设置信号量的访问权限标志,函数调用成功时返回信号量ID,失败则返回-1。
int semop (int semid, struct sembuf *spos, int nspos);
对信号量进行操作的函数,用于改变信号量的键值,semid是信号量的标志,spos是指向一个结构体数组的指针,表明要进行什么操作,nspos表明数组的元素个数,调用成功则返回0,失败则返回-1.
int semctl (int semid, int semnum, int cmd, union semun arg);
该函数得作用是对信号量进行一系列得控制,semid是要操作得信号量标志,semnum是信号量得下标,cmd是操作的命令,经常用的两个命令是:SETVAL、IPC_RMID,arg用于设置或返回信号量信息。
信号量的作用在于实现进程间的同步机制,在介绍完共享内存之后,我们共享内存和信号量实现一个类似于 QQ 的进程同步聊天工具。
二,共享内存
共享内存是系统创建的特殊地址空间,允许不相关的多个进程使用这个内存空间,即多个进程能够使用同一个内存中的数据。
共享内存与其他进程通信方式相比较,不需要复制数据,直接读写内存,是一种效率非常高的进程通信方案。但它本身不提供同步访问机制,需要我们自己控制(信息量)。在Linux中,只要把共享内存段连接到进程的地址空间中,这个进程就可以访问共享内存中的地址了。
Linux系统提供的共享内存操作函数与信号量,消息队列等类似,主要有以下几个:
(1) int shmget(key_t key,int shmsz,int shmflg);
(2) void *shmat(int shmid,const void *shmaddr, int shmflg);
如果shmaddr为0。则此段连接到由内核选择的第一个可用地址上,这是推荐的使用方式。
如果shmaddr非0,并且没有指定SHM_RND,则此段链接到addr所指的地址上。
如果shmaddr非0且指定SHM_RND,则此段链接到shmaddr - (addr mod ulus SHMLBA)所表示的地址上。
共享内存的主要操作函数主要有以下:
Shmget()函数分配一块新的共享内存。shmget()函数调用成功则返回共享内存的ID;否则返回-1.
Shmat()函数的作用是连接共享内存与某个进程的地址空间。
Shmdt()函数用来解除进程与共享内存区域的关联,使当前进程不能继续访问共享内存。
Shmctl()函数实现对共享内存区域的控制操作。其用法与消息队列的msgctl()函数类似。
下面是一个将信号量和共享内存联合起来达到同步聊天的程序,思路是这样的服务器和客户端根据两个信号量进行沟通,信号量1负责服务器的写和客户端的读,而信号量2负责服务器的读和客户端的写(因为半双工管道的原因,不能同时控制读写)。而读写的信息存放在共享内存中,服务器写进数据到共享内存,并发出消息给客户端,客户端在内存中取出数据读。而信号量的作用就在于同步控制,根据0阻塞进程的特性,防止服务器自己写的数据被自己读的错误发生。
具体实现见下面代码解析。
服务器代码:
int main(int argc,char *argv[])
{
key_t key_id; //得到唯一的key_id值。
key_id = ftok(argv[1],ID); //ftok()上面有解析,ID=OxFF系统随机的唯一值
if(key_id == -1){ //要是返回-1 ftok得到key_id失败
exit(1);
}
printf("key_id = 0x%x\n",key_id);
key_t shm_id; //用唯一的key值申请信息量
shm_id = shmget(key_id, 1024, IPC_CREAT|IPC_EXCL|0666);//8进制的666权限
char *addr;
addr = (char *)shmat(shm_id,NULL,0); //申请公用内存并与addr连接
key_t sem_key;
sem_key = ftok(argv[1],ID1);
key_t sem_id = semget(sem_key, 2, IPC_CREAT|IPC_EXCL|0666);
union semun init;
init.val = 0;
semctl(sem_id, 0, SETVAL, init);
semctl(sem_id, 1, SETVAL, init);
struct sembuf p = {0, -1, 0}; //p操作:对0号信息量-1 请与客户端的对比
struct sembuf v = {1, 1 ,0}; //v操作:对1号信息量+1
while(1){
printf("Ser:>");
scanf("%s",addr); //输入信息存入内存
if(strcmp(addr,"quit")==0){
semop(sem_id,&v,1);
shmctl(shm_id,IPC_RMID,NULL);
semctl(sem_id,0,IPC_RMID);
semctl(sem_id,1,IPC_RMID);
break;
}
semop(sem_id, &v, 1); //执行V操作,同步的关键!
semop(sem_id, &p, 1);
printf("Cli:>%s\n",addr);
}
return 0;
}
客户端的代码如下:
int main(int argc, char *argv[])
{
key_t key_id;
key_id = ftok(argv[1], ID);
key_t shm_id = shmget(key_id,0,0); //取得服务器的信息量
char *addr = (char *)shmat(shm_id, NULL, 0);
key_t sem_key;
sem_key = ftok(argv[1],ID1);
key_t sem_id = semget(sem_key,0,0); //取得服务器公用内存的key值
struct sembuf p = {1, -1, 0}; //P操作:对1号信息量—1 与服务器的刚好相反
struct sembuf v = {0, 1, 0}; //V操作:对0号信息量+1
while(1)
{
semop(sem_id, &p, 1);
printf("Ser:>%s\n",addr);
printf("Cli:>");
scanf("%s",addr);
if(strcmp(addr,"quit") == 0){
semop(sem_id, &v, 1);
break;
}
semop(sem_id, &v, 1);
}
return 0;
}
代码实现的逻辑图如下:
当服务器输入数据时,v操作使得1号信息量加1,使得1号信息量从默认值0变为1。这个时候客户端就能读取数据了,客户端读取需进行P操作,而之前由于P操作会使得1号信息量从默认值0,减去1变成负数,标志资源不足,进程阻塞在此,是不可读状态。只有服务器写了,V操作后客户端才可以读取并进行P操作。同理服务器的读和客户端的写是同样的道理。利用信息量的阻塞状态,来达到同步机制,是IPC的重要知识!
附上运行该程序实现进程间通信的截图:
三,消息队列
消息队列是将消息按队列的方式组织成的链表,每个消息都是其中的一个节点。
消息队列的运行方式与命名管道非常相似。欲与其他进程通信的进程只要将消息发送到消息队列中,目的进程就从消息队列中读取需要的消息。需要注意的是,消息队列的长度以及每个消息的大小都是有限制的。
Linux系统提供的消息队列操作函数主要有以下几个:
int msgget(key_t key,int msgflg)函数与信号量的semget()函数相似,作用是创建一个消息队列。参数key是一个键值,可由用户设定也可通过ftok()函数获得。
int msgsnd(int msqid, const void *msgptr, int msgsz,int msgflg);函数的作用是将消息发送到消息队列中去。Msqid为消息队列ID。Msgptr是指想要发送的消息的指针,并且指向的缓冲区得第一个字段应为长整形,指定消息类型,消息内容存放在该缓冲区得紧跟消息类型字段得区域中。
int msgrcv(int msqid, void *msgptr, int msgsz, long msgtyp, int msgflg);函数的作用是从消息队列中读取一个消息。
消息队列在IPC中非常重要,是同信的基本手段之一,比如刚才的聊天程序用消息队列也可以实现,不过消息队列要求进程间的信息传递需要附加该信息的类型,所以我们将信息定义成如下结构体:
typedef struct Msg{
long MsgType;
char MsgText[256];
}Msg;
服务器代码如下:
include "msg.h"
#define MSGSND 100 //消息队列实现的核心是标志量
#define MSGRCV 200
int main(int argc, char *argv[])
{
key_t msg_key;
msg_key = ftok(argv[1],ID);
key_t msg_id;
msg_id = msgget(msg_key,FILE_MODE);
Msg msg;
while(1){
printf("Ser:>");
scanf("%s",msg.MsgText);
if(strcmp(msg.MsgText,"quit") == 0){
msgctl(msg_id,IPC_RMID,NULL);
break;
}
msg.MsgType = MSGSND; //发送时必须附加标志量
msgsnd(msg_id, &msg, strlen(msg.MsgText)+1,IPC_NOWAIT);
msgrcv(msg_id, &msg, 256, MSGRCV, 0); //接收函数的参数里也含标志量
printf("Cli:>%s\n",msg.MsgText);
}
return 0;
}
客户端代码实现:
#define MSGSND 200 //注意客户端的发送标志量和服务器的接收量相同
#define MSGRCV 100 //同理,接受标志量和服务器发送标志量相同
int main(int argc, char *argv[])
{
key_t msg_key = ftok(argv[1],ID); //ID相同
key_t msg_id = msgget(msg_key,0);
Msg msg;
while(1)
{
msgrcv(msg_id, &msg, 256, MSGRCV, 0);
printf("Ser:>%s\n",msg.MsgText);
printf("Cli:>");
scanf("%s",msg.MsgText);
if(strcmp(msg.MsgText,"quit") == 0){
break;
}
msg.MsgType = MSGSND;
msgsnd(msg_id, &msg, strlen(msg.MsgText)+1, IPC_NOWAIT);
}
return 0;
}
可见消息队列同样实现了进程间的通信,不过这种通信不是同步的。需要标志量来区分不同信道传送的信息,相当于队列的方式,从这头端进程发送进从另一端进程接受。