内核提供,单工,自同步机制
自同步机制:迁就慢的那一方,两个进程在使用管道通信时,一端为读端,一端为写端。假设读端很快,写端速度很慢,那么读端很快就会将管道给读空,如果当前管道读空,但是写端仍然存在的话,作为读端,你要一直在那等待,等到写端写数据到管道)。
匿名管道:pipe();
命名管道:mkfifo();
XSI IPC源自于系统V的IPC功能。
IPC是进程间通信Inter-Process Communication的缩写,
有三种IPC我们称作XSI IPC,即消息队列、信号量数组以及共享存储器,它们之间有很多相似之处。
比如他们都能够既进行血缘进程间通信,也能进行非血缘间进程通信。
主动端:先发包的一方
被动段:先收包的一方(先运行)
我们使用命令ipcs,其中s表示的是show。
Message Queues:消息队列
Semaphore Arrays:信号量数组
Shared Memory Segments:共享存储器
图中,还有一个很重要的参数,key,通信双方要拿到同一个key。
拿到key值需要使用一个函数:ftok();
NAME
ftok - convert a pathname and a project identifier to a System V IPC key
将路径名和项目标识符转换为System V IPC键
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
RETURN VALUE
On success, the generated key_t value is returned. On failure -1 is returned, with errno indicating the error as for the stat(2) system call.
还有,对于上面这三个进程间通信方式,他们都有一个共同的特点。那就是创建使用xxxget、操作使用xxxop、销毁或进行其他控制使用xxxctl
消息队列相关函数的前缀一般使用msg
信号量数组相关函数的前缀一般使用sem
共享存储器相关函数的前缀一般使用shm
因此,如果我们想要查看消息队列创建的函数,就可以使用命令 man msgget
首先来看消息队列的使用。
消息队列的创建msgget
NAME
msgget - get a System V message queue identifier
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
创建消息队列时,参数msgflg使用:IPC_CREAT,与创建文件时一样,msgflg也是一个位图,创建时一定要给一个权限。
RETURN VALUE
If successful, the return value will be the message queue identifier (a nonnegative integer), otherwise -1 with errno indicating the error.
消息队列的操作msgop
NAME
msgrcv, msgsnd - System V message queue operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//发送
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数msgp是要发送哪块空间的数据。
参数msgsz是要发送数据的大小。
参数msgflg是发送要求。
//接收
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
int msgflg);
参数msqid是消息队列的编号。
参数msgp是要接收到哪块空间。
注意这个参数的数据类型是void *,实际上并不是真正的void*,只是这个数据类型被隐藏起来了。
The msgp argument is a pointer to a caller-defined structure of the following general form:
msgp参数是一个指向调用者定义(我们事先定义好)的结构体的指针,其一般形式如下:
The msgp argument is a pointer to a caller-defined structure of the following general form:
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
并不一定要叫做msgbuf这个名字,只是说成员应该大概与这里一样。
第一个参数long mtype; 必须是一个大于0的long整型数。
第二个参数char mtext[1];数组长度为1,这表明当前结构体是一个可变长的结构体。也就是说,数组长度定义成你需要的大小。
这些要在我们的协议中定义的结构体中定义。
第三个参数msgsz是当前要接收的有效字节数。
第四个参数long msgtyp是你是否要挑选消息来收取,比如说,当前消息队列中有10个包,将这个参数设置成你要挑选的消息对应的编号数值。
第五个参数msgflg是一些特殊要求,由一些宏来指定。
比如IPC_NOWAIT,没有消息立马返回。
MSG_NOERROR
To truncate the message text if longer than msgsz bytes.
如果接收到的消息长于指定的msgsz字节,将被截断到msgsz字节。
RETURN VALUE
On failure both functions return -1 with errno indicating the error, otherwise msgsnd() returns 0 and msgrcv() returns the number of bytes actuallycopied into the mtext array.
消息队列的销毁或其他操作msgctl
NAME
msgctl - System V message control operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
其中比较常用的cmd有:
IPC_RMID:作用是移除消息队列。使用IPC_RMID后,后一个参数就不需要了。
消息队列是双工的,大家都可以往消息队列上发送消息,也可以从消息队列上接收消息。
现在,我们使用消息队列来实现学生信息的收发的功能。
我们新建一个proto.h的文件,用来定义通信双方的通信协议。
我们新建一个receiver.c的文件,作为接收方。
我们新建一个sender.c的文件,作为发送方。
代码如下:
proto.h
//制定通信协议
#ifndef _PROTO_H
#define _PROTO_H
//要想拿到同一个key,需要哪些参数
/*
key_t ftok(const char *pathname, int proj_id);
*/
//是一个目录
#define KEYPATH "/work/thread_code/semaphore/msg/test/"
//任意写一个整型数据,比如
//#define KEYPROJ 123
//但是一般认为没写单位(前缀表明是几进制)的整型数,不确定它的含义.
//一般可以定义成字符型,这样转换到程序中一定是整型数.
#define KEYPROJ 'c'
#define NAMESIZE 32
//发送方要发送这样类型的结构体
//接收方要按照这种类型的结构体来解析接收到的数据
struct msg_st
{
long mtype;
char name[NAMESIZE];
int math;
int chinese;
};
#endif
receiver.c
//负责接收学生信息
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include "proto.h"
int main()
{
key_t key;
int msgid;
struct msg_st rbuf;
//获得key值
key = ftok(KEYPATH,KEYPROJ);
if(key<0)
{
perror("ftok()");
exit(1);
}
//为什么先写被动端,因为被动端先运行起来,
//当被动方写了IPC_CREAT之后,主动方可以不用再写IPC_CREAT
//但是,如果主动方写了IPC_CREAT,被动方仍然也要写.
//创建消息队列
msgid = msgget(key,IPC_CREAT|0600);
if(msgid < 0)
{
perror("msgget()");
exit(1);
}
while(1)
{
//接收来自消息队列的信息
if(msgrcv(msgid,&rbuf,sizeof(rbuf)-sizeof(long),0,0)<0)
{
perror("msggrcv()");
exit(1);
}
printf("NAME = %s.\n",rbuf.name);
printf("MATH = %d.\n",rbuf.math);
printf("CHINESE = %d.\n",rbuf.chinese);
}
return 0;
}
sender.c
//负责发送学生信息
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include "proto.h"
#include <string.h>
#include <math.h>
#include <sys/msg.h>
int main()
{
key_t key;
int msgid;
struct msg_st sbuf;
//获得key值
key = ftok(KEYPATH,KEYPROJ);
if(key<0)
{
perror("ftok()");
exit(1);
}
//创建消息队列
msgid = msgget(key,0);
if(msgid < 0)
{
perror("msgget()");
exit(1);
}
sbuf.mtype = 1;
strcpy(sbuf.name,"chantui");
sbuf.math=rand()%100;
sbuf.chinese = rand()%100;
//作为发送方,要调用msgsnd
if(msgsnd(msgid,&sbuf,sizeof(sbuf)-sizeof(long),0)<0)
{
perror("msgsnd()");
exit(1);
}
//谁创建谁销毁,我没创建,所以我也不用销毁
//msgctl();
puts("ok");
return 0;
}
编译完成之后,先运行receiver程序,程序运行后,由于当前队列中没有消息,因此会在运行等待。
我们再重新打开一个终端来运行sender程序。
此时在receiver会接收到消息。
我们执行命令ipcs会看到消息队列的一些信息。
假如,我先运行sender程序,
再来运行receiver程序,又会怎么样呢?
也能接收到,那这是为什么呢?
我们可以把消息队列理解成有一个有缓存消息的能力。那这个缓存能力有多大呢?
我们可以通过命令 ulimit -a 这个命令来查看。
我们也可以使用命令 ulimit -q 来对这个空间大小进行修改。
还有一个问题,当我们使用ctrl+c来终止程序后,为什么还能够查看到key的值。
答:我们使用ctrl+c来终止程序,程序相当于是异常终止。
我们在程序中使用的是用while循环来接收消息。程序没有机会执行到下面的代码。
//销毁消息队列
msgctl(msgid,IPC_RMID,NULL);
消息队列自然就继续存在。
那怎么才能解决这个问题呢?
1、我们可以在程序中使用信号的机制(增加信号捕获和处理函数)。
2、我们可以使用命令ipcrm
我们使用man ipcrm来查看手册看下ipcrm命令的具体使用方法。
-M, --shmem-key shmkey
Remove the shared memory segment created with shmkey after the last detach is performed.
根据共享内存的Key,来删除共享内存。
-m, --shmem-id shmid
Remove the shared memory segment identified by shmid after the last detach is performed.
根据共享内存的id,来删除共享内存。
-Q, --queue-key msgkey
Remove the message queue created with msgkey.
根据消息队列的key ,来删除消息队列。
-q, --queue-id msgid
Remove the message queue identified by msgid.
根据消息队列的id,来删除消息队列。
-S, --semaphore-key semkey
Remove the semaphore created with semkey.
根据信号量的key,来删除信号量。
-s, --semaphore-id semid
Remove the semaphore identified by semid.
根据信号量的id,来删除信号量。
比如我们根据消息队列的id来删除这个消息队列,我们就可以使用命令ipcrm -q 0
在使用ipcs命令来查看下。
已经被删除了。
在上面的例子中还存在两个问题。
1、结构体成员long mtype的用处。
2、互动的问题(snder方只负责发,receiver方只负责接收),这里要做成双方既可以接收也可以发送。
在下面将会得以解答。
我们这里来完成一个ftp实例。
C:客户端Client
S:服务器端Server
如图所示,首先,C端先向S端发送一个请求,请求其某个路径下的某个文件,我们把它简称为PATH,然后,S端找到这个文件之后呢,就把这个文件(数据)发送给C端。当然,有可能这个文件很大,不能一次性发送过去,需要分n次发送才能完成,因此我们还要在文件的结尾再发送一个EOF(End of Transmission)标志。
当然,C端向S端请求的文件必须是有权限的常规文件,S端找到这个文件之后呢,首先要按规定的大小,一步步读取到S端的进程中,然后再发送出去。
当程序写好之后,C端和S端哪个应该先运行呢?
答:我们从主动端和被动端来考虑这个问题,C端是发送请求的,S端是接收请求的,所以S端应该是被动端,所以S端应该先运行起来。此外,最好还应该把S端做成守护进程的形式,让它可以一直在后台运行。这样我就可以随时来请求文件。
当然,他们两个应该运行在两个终端上,但这里模拟时,两个程序都运行在同一个设备上,也就是自己既是C端也是S端。
我们这里尝试使用状态机来实现这一功能。
对于网络通信和两个无血缘关系的进程通信时,我无法知道你的下一个包是什么,那怎么办呢?
下面提出两种规划。
规划一:使用共用体
新建 proto.h 文件,
#ifndef _PROTO_H_
#define _PROTO_H_
//为了获得同一个Key值
#define KEYPATH "/etc/services"
#define KEYPROJ 'a'
#define PATHMAX 1024 //路径名长度最多为1024
#define DATAMAX 1024 //数据包大小最多为1024
//会产生三种可能的数据包
enum
{
MSG_PATH=1,
MSG_DATA,
MSG_EOT
};
//path包是S端有可能接收到的包
//C端可能接到EOT包,也可能接到data包,无法确定.
//路径包
struct msg_path_st
{
long mtype; /* must be MSG_PATH */
char path[PATHMAX]; /* ASCIIZ带尾0的串 */
}msg_path_t;
//文件数据包
struct msg_data_st
{
long mtype; /* must be MSG_DATA */
char data[DATAMAX];/*带尾0的数据包*/
//如果发过来的是空洞文件(文件中有一部分或者全部都是0的情况)
//因此,让它自述长度,也就是使用datalen来描述data包中有效数据是多少
int datalen;
}msg_data_t;
//表示文件结束传输的EOT包
typedef struct msg_eot_st
{
long mtype; /* must be MSG_EOT */
}msg_eot_t;
//两个无血缘关系的进程通信时,我无法知道你的下一个包是什么
//那怎么办呢?
//可以使用联合体
/*
结构体与共用体相比,最大的特点是成员属于合作关系。内存空间大小是按照结构体成
员定义顺序来分配的。但是,共用体则不同,共用体中的成员是敌我关系。同一时刻,只能
有一个成员生效。根据当前成员所占内存最大的那个来分配空间大小
*/
/*
因为接收端同一时刻只能接收到一种包
虽然同一时刻这三者只会存在一者,但是不管我接收哪一种包,前四个字节都是mtype,于是>根据该值就可以判断出是哪一种包.
*/
union msg_s2c
{
long mtype;
msg_data_t datamsg;
msg_eot_t eotmsg
};
#endif
规划二:将DATA 包和 EOT 包综合为1个包(有缺陷)
在当前,DATA 包和 EOT 包实际上有很多地方是共通的,比如都有 long mtype,因此可以将这两个包给综合为1个包。之后再根据datalen的值进行判断,比如这里规定 datalen > 0 为 data包。datalen == 0 为 eot包 。
这种写法是有缺陷的,是不如协议一中使用共用体所规划的。因为这里只有两种包即MSG_DATA or MSG_EOT,如果再多一种包就不行了。
新建 proto2.h 文件,
#ifndef _PROTO2_H_
#define _PROTO2_H_
//为了获得同一个Key值
#define KEYPATH "/etc/services"
#define KEYPROJ 'a'
#define PATHMAX 1024 //路径名长度最多为1024
#define DATAMAX 1024 //数据包大小最多为1024
//会产生三种可能的数据包
enum
{
MSG_PATH=1,
MSG_DATA,
MSG_EOT
};
//path包是S端有可能接收到的包
//C端可能接到EOT包,也可能接到data包,无法确定.
//路径包
struct msg_path_st
{
long mtype; /* must be MSG_PATH */
char path[PATHMAX]; /* ASCIIZ带尾0的串 */
}msg_path_t;
//文件数据包
//这种写法是有缺陷的,是不如协议一中使用共用体所规划的。
//因为这里只有两种包即MSG_DATA or MSG_EOT,如果再多一种包就不行了。
struct msg_s2c_st
{
long mtype; /* must be MSG_DATA or MSG_EOT */
char data[DATAMAX];/*带尾0的数据包*/
//如果发过来的是空洞文件(文件中有一部分或者全部都是0的情况)
//因此,让它自述长度,也就是使用datalen来描述data包中有效数据是多少
int datalen;
/*
* datalen > 0 : data包
* datalen == 0 : eot包
*/
}msg_data_t;
#endif
对于这个的实现,放在了网络通信那里。