XSI IPC结构:源自System V UNIX(1983)的IPC功能(为了满足进程间通信,首次引入三中进程间通信机制(IPC):消息队列、信号量、共享内存)。
IPC结构(消息队列、信号量、共享内存)通过共同的IPC资源通信,而IPC资源是通过内核中IPC结构的一个非整数的标识符来区分的,类似于管道通信的文件描述符(XSI IPC不使用文件系统名字空间,构造了自己的名字空间)。但是IPC结构的标识符是内部名,进程无法看到IPC的内部标识符,所以每个IPC对象都会与一个键相关联,作为外部名供进程访问。
XSI IPC结构的问题:
(1)IPC结构是内核资源,没有访问计数,不知道是否有进程访问该资源(不知道什么时候释放该资源,造成资源浪费)。例如,当进程创建了一个消息队列,在该队列放入了几则消息,然后终止。但是该消息队列及其内容并不会被删除。直到进程调用msgrcv或msgctl读消息或删除消息队列;或者ipcrm命令删除消息队列。
(2)IPC结构有自己的名字空间,所以文件系统没有其名字。为了支持访问他们或修改访问权限,不得不增加新的系统调用函数和命令。
IPC命令:
- ipcs -m - 显示共享内存(m: memory)
- ipcs -q - 显示消息队列(q: queue)
- ipcs -s - 显示信号量(s: semphore)
- ipcs -a - 显示所有IPC对象(a: all)
- ipcrm -m ID - 删除共享内存
- ipcrm -q ID - 删除消息队列
- ipcrm -s ID - 删除信号量
(3)因为IPC不使用文件描述符,所以不能对他们使用多路转接I/O函数:select和poll。这就使得难于一次使用多个IPC结构,以及在文件或设备I/O中只用IPC结构。例如,没有某种形式的忙等待循环,就不能使一个服务器进程等待将要放在两个消息队列任一个中的消息。
消息队列:
消息队列是消息的链接表,存放在内核中并由消息队列标识符标识。内核对象来维护消息,充当通讯的中间人,多用于多进程间通信。
消息队列提供了一种“数据 + 类型”的数据块通信的方法,通信双方看不到队列的消息,只能向队列发送和获取消息,这也避免了命名管道的同步和阻塞问题。但是与管道一样,每条消息的大小都会有一的最大长度限制,内核队列维护的全部消息总长度也有一个上限,如果超出会引起消息队列函数失败。Linux提供两个宏定义MSGMAX(消息的最大长度)和MSGMNB(队列的最大长度)。
消息队列函数的定义:
1、创建或者访问消息队列:
int msgget(key_t key, int msgflg); //成功返回一个正整数,失败返回-1
// key(键值来命名摸个特定的消息队列),msgflg(权限 | IPC_CREAT(或上创建消息队列))
2、把消息添加到消息队列(副本)中:
int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg); //成功返回0,失败返回-1
//mspid(msgget函数返回的消息队列标识符),
//msg_ptr(指向一个包含long类型的数据段),
//msg_sz(数据(不包括long变量的大小)的大小),
//msgflg(队列满时,设置IPC_NOWAIT,函数立即返回,不发送消息并返回-1;若没有设置,队列满时进程则阻塞)
消息的长度必须小于系统规定上限,还必须以一个长整型成员变量开始,由这个长整型成员来确定消息类型。
strct my_message
{
long int message_type;
/* your data*/
}
3、从消息队列中获取消息:
int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg);
//成功返回放到接收区中的字节数,消息被复制到msg_ptr指向用户分配的缓存区,然后删除对应消息,失败返回-1
//mspid(msgget函数返回的消息队列标识符),
//msg_ptr(接收消息的的指针,指向一个包含long类型的数据段,同msgsend),
//msg_sz(数据(不包括long变量的大小)的大小),
//msgtype(接受优先级,0为队列第一个消息,大于0为相同的类型的消息,小于0为<=绝对值消息)
//msgflg(队列空时,设置IPC_NOWAIT,函数立即返回,返回-1;若没有设置,队列空时进程则阻塞)
4、删除内核对象(IPC结构)
int msgctl (int msqid, int cmd, struct msqid_ds *buf); //成功返回0,失败返回-1
//mspid(msgget函数返回的消息队列标识符),
//cmd(采取的动作,IPC_RMID删除消息队列),
/*msqid_ds结构至少包含成员:
struct msqid_ds
{
uid_t msg_perm.uid;
uid_t msg_perm.gid;
mode_t msg_perm.mode;
}
*/
//进程A发送数据信息
//定义数据
typedef struct msgdata
{
long mtype;
char mtext[128];
}MsgData;
int main()
{
//创建消息队列
int msgid = msgget((key_t)1234, 0664 | IPC_CREAT);
assert(msgid != -1);
//填写信息(类型+数据)
MsgData data;
memset(&data, 0, sizeof(data));
data.mtype = 100;
strcpy(data.mtext, "hello");
//发送信息(类型:100,数据:"hello\n")
msgsnd(msgid, &data, strlen(data.mtext), 0);
//填写信息(类型+数据)
memset(&data, 0, sizeof(data));
data.mtype = 200;
strcpy(data.mtext, "world");
//发送信息(类型:200,数据:"world\n")
msgsnd(msgid, &data, strlen(data.mtext), 0);
//不能发送完后直接删除消息队列
//msgctl(); 删除立即生效
}
//进程B接受信息
typedef struct msgdata
{
long mtype;
char mtext[128];
}MsgData;
int main()
{
int msgid = msgget((key_t)1234, 0664 | IPC_CREAT);
assert(msgid != -1);
MsgData data;
memset(&data, 0, sizeof(data));
//只接受到类型为100的信息
msgrcv(msgid, &data, 127, 100, 0);
printf("data.type: %d\n", data.mtype);
printf("data.text: %s\n", data.mtext);
}
信号量:
信号量相当于一个计数器(不传送数据),用于控制多进程对共享数据对象(临界资源)的访问。
信号量的使用:
1、创建一个新信号量或取得一个已有信号量的键:
int semget(key_t key, int num_sems, int sem_flags);
//key(键值来访问同一个信号量),num_sems(信号量的个数),sem_flags(权限 | IPC_CREAT(或上,创建新的信号量)
2、改变信号量:
int semop(int sem_id, struct sembuf * sem_ops, size_t num_sem_ops);
//sem_id (由semget返回的信号量标识符),
//sem_ops(指向一个结构体数组的指针,每个数组至少包括以下成员)
/*struct sembuf
{
short sem_num;//信号量编号,指定信号量集中的某一个资源
short sem_op;//-1P操作,+1V操作
short sem_flg;//SEM_UNDO跟踪信号量的修改情况
}*/
//num_sem_ops(结构体数组的个数)
3、控制信号量信息:
int semctl(int sem_id, int sem_num, int command,/*union semun val*/);
//sem_id(semget返回的信号量标识符)
//sem_num(信号量编号,0为第一个信号量)
//command(采取的动作)
/*union semun
{
int val; //SETVAL初始化信号量,IPC_RMID删除信号量标识符
struct semid_ds *buf;
unsigned short *array;
}*/
注意:
1、信号量并非是单个,而是一个含有一个或多个信号量的集合,所以创建时,需指出集合中信号量的数量。
2、不能原子的创建一个信号量集合并赋值,因为创建信号量(semget)与赋值初始值(semctl)分开。
共享内存:
进程间通过共享一段内存空间来进行通信。省去了数据拷贝的时间和访问内核的时间,所以是最快的一种IPC。
因为没有了内核的维护,进程直接操作物理空间,所以共享内存没有提供同步机制、这就需要我们处理进程同步问题(信号量)。
共享内存的使用:
1、创建共享内存
int shmget(key_t key, size_t size, int flag);
//key(键值来访问同一个共享内存),size(共享的内存容量),flag(9个比特权限 | IPC_CREAT)
2、将共享内存连接进程中(连接的进程虚拟地址):
void *shmat(int shm_id, const void *shm_addr, int shmflg); //成功返回连接的进程虚拟地址
//shm_id(shmget返回的共享内存标识符)
//shm_addr(指定连接到进程中的地址位置,NULL(由系统选择))
//shmflg(0 读写方式连接此段)
3、断开连接:
int shmdt(void *addr); //调用成功共享内存引用计数减一
//addr(shmat返回值,连接的进程虚拟地址)
4、删除内核对象:
int shmctl(int shmid, int cmd, strcut shmid_ds *buf);
//shm_id(shmget返回的共享内存标识符)
//cmd(IPC_RMID 删除共享内存的标识符,其他进程不能连接此段。若该进程为最后一个使用,则删除;否则只是计数减一)
//buff(指向包含共享内存模式和访问权限的结构)