引言:
之前学习的进程之间交换信息的方法只能由fork或exec传送打开文件,或者文件系统。但是这种通讯方式有局限性,接下来将说明进程之间相互通信的其他技术——IPC(InterProcessCommunication),过去UNIX系统IPC是各种进程间通信方式的统称,但是,其中极少能在所有UNIX系统中实现移植。随着POSIX和Open Group(以前是X/Open)标准化的推进和影响扩大,情况随已得到改善,但是差别仍然存在。以下将介绍几种实现所支持的不同形式的IPC。
单机版进程间通信方式:
-
半双工管道(包括无名管道和命名管道)
-
消息队列
-
信号量
-
共享内存
多机版进程间通讯方式:
- 套接字(socket)
- streams
管道(父进程创建):
概念:
管道,通常指无名管道,是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”,本质:内核缓冲区。
特点:
- 它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端,当数据从管道中读取后管道中就没有了。
- 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。本质:内核缓冲区.
如何建立半双工管道:
管道是由调用pipe函数来创建
#include <unistd.h>
int pipe(int pipefd[2]);函数的参数是:含有两个元素的整型数组
返回值:返回:成功返回0,出错返回-1
fd参数返回两个文件描述符,fd[0]指向管道的读端read(fd[0],-,-)
其中read()函数如果读不到东西,会阻塞。
fd[1]指向管道的写端,write(fd[1],-,-)。向管道文件读写数据其实
是在读写内核缓冲区。要关闭管道只需要将读端和写端close掉即可。fd[1]的写出是fd[0]的读入。0对应标准输入,1对应标准输出一样。
默认标准输入对应的设备是键盘,标准输出和标准错误对应的是显示器
linux下一起皆文件,设备一定是文件,文件不一定是设备,标准输入
和标准输出就不是文件,他们是链接文件,什么是链接文件?文件内容
是另一个文件的地址的文件称为链接文件。
标准输入、标准输出解释
父进程给子进程信息:
#include <unistd.h>
#include<stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<string.h>
#include<stdlib.h>
int main()
{int fd[2];int status;pid_t fpid;char* writebuf=NULL;writebuf=(char*)malloc(1024);printf("请输入父进程要传递给子进程的消息:\n");scanf("%[^\n]",writebuf);if(pipe(fd)==-1){printf("creat file fail\n");}fpid=fork();if(fpid<0){printf("创建子进程失败\n");perror("fork");}else if(fpid>0){close(fd[0]);write(fd[1],writebuf,strlen(writebuf));waitpid(fpid,&status,0);if(WIFEXITED(status)){printf("子进程正常结束,状态值是:%d\n",WEXITSTATUS(status));}close(fd[1]);}else{close(fd[1]);char* readbuf=NULL;int n_read;readbuf=(char*)malloc(strlen(writebuf));n_read=read(fd[0],readbuf,strlen(writebuf));printf("子进程得到数据:%s,字节数是:%d\n",readbuf,n_read);close(fd[0]);}return 0;
}这个程序:起初当输入字符串中间有空格时,子进程只会收到空格之前的字符串
问题诊断:scanf()遇到空格、回车、Tab则认为输入结束,后面的就不会当做输入了
解决方法:%s换为%[^\n](^有非的意思,即不遇到\n不停止)或者用gets()scanf输入字符串的时候不会接收Space空格,回车Enter,Tab键,则认为输入结束。
scanf函数直接从输入缓冲区中取数据,而并非从键盘(也就是终端)缓冲区读取。1、空格:空格键产生的字符,ascii码十进制:32
2、空字符:字符串结束标志‘\0’,为被动添加,ascii码十进制:0
3、字符0:ascii码十进制:48
子进程发送消息给父进程:
#include <unistd.h>
#include<stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<string.h>
#include<stdlib.h>
int main()
{int fd[2];int status;pid_t fpid;char* writebuf=NULL;writebuf=(char*)malloc(1024);printf("请输入父进程要传递给子进程的消息:\n");scanf("%[^\n]",writebuf);if(pipe(fd)==-1){printf("creat file fail\n");}fpid=fork();if(fpid<0){printf("创建子进程失败\n");perror("fork");}else if(fpid>0){sleep(1);close(fd[1]);char* readbuf=NULL;int n_read;readbuf=(char*)malloc(strlen(writebuf));n_read=read(fd[0],readbuf,strlen(writebuf));waitpid(fpid,&status,0);printf("父进程得到数据:%s,字节数是:%d\n",readbuf,n_read);if(WIFEXITED(status)){printf("子进程正常结束,状态值是:%d\n",WEXITSTATUS(status));}}else{close(fd[0]);write(fd[1],writebuf,strlen(writebuf));}return 0;
}
memcpy()函数介绍、scanf输入字符串遇到空格?
FIEO(有名管道,半双工):
无名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道(FIFO),也叫有名管道、FIFO 文件。命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
命名管道(FIFO)和无名管道(pipe)有一些特点是相同的,不一样的地方在于:
- FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。
- 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
- FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。
- 另外,使用统一fifo文件,可以有多个读端和多个写端。
创建有名管道:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);pathname: 普通的路径名(就是创建的管道的位置),也就是创建后 FIFO 的名字。
mode: 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同返回值:成功:0失败:如果文件已经存在,则会出错且返回 -1。如果因为文件存在引起的错误,则会返回-1并将errno的值赋EEXIST可以用下面的代码找出其他的出错原因
if(mkfifo("./fifodir",0666)==-1&&errno!=EEXIST){printf("管道创建失败\n");perror("because");
}
注意:
当进程对命名管道的使用结束后,命名管道依然存在于文件系统中,除非对其进行删除操作。命名管道的数据读取后也会消失(不能反复读取),即且严格遵循先进先出的规则。因此,每次命名管道文件使用完后,其大小为0字节,不会产生中间临时文件。
命名管道的默认操作:
- 后期的操作,把这个命名管道当做普通文件一样进行操作:open()、write()、read()、close()。但是,和无名管道一样,操作命名管道肯定要考虑默认情况下其阻塞特性。
- 下面验证的是默认情况下的特点,即 open() 的时候没有指定非阻塞标志( O_NONBLOCK )。open() 以只读方式打开FIFO 时,要阻塞到某个进程为写而打开此 FIFO。open() 以只写方式打开 FIFO 时,要阻塞到某个进程为读而打开此 FIFO。
- 简单一句话,只读等着只写,只写等着只读,只有两个都执行到,才会往下执行。
- 在这里我们需要注意一点,就是不能以 O_RDWR 方式打开管道文件,这种行为是未定义的。倘若有一个进程以读写方式打开了某个管道,那么该进程写入的数据又会被该进程本身读取,而管道一般只用于进程间的单向数据通信。
管道打开方式:
- (1)只读且阻塞方式
open(const char *pathname, O_RDONLY); - (2)只读且非阻塞方式
open(const char *pathname, O_RDONLY | O_NONBLOCK); - (3)只写且阻塞方式
open(const char *pathname, O_WRONLY); - (4)只写且非阻塞方式
open(const char *pathname, O_WRONLY | O_NONBLOCK);
读端代码:
#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<errno.h>
#include<string.h>
int main()
{char readbuf[30];int n_read;int fd;memset(readbuf,'\0',30);if(mkfifo("./fifodir",0600)==-1&&errno!=EEXIST){printf("mkfifo fail\n");perror("mkfifo");}fd=open("./fifodir",O_RDONLY);while(1){n_read=read(fd,readbuf,30);printf("读取到%d个字节,内容是%s\n",(int)strlen(readbuf),readbuf);}close(fd);return 0;
}
写端代码:
include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<errno.h>int main()
{int fd;fd=open("./fifodir",O_WRONLY);while(1){write(fd,"hello reader",20);sleep(1);}close(fd);return 0;
}
消息队列、消息队列参数详解
什么是消息队列?
- 消息队列是消息的链表,存放在内核中并由消息队列标识符表示。
- 消息队列提供了一个从一个进程向另一个进程发送数据块的方法,每个数据块都可以被认为是有一个类型,接受者接受的数据块可以有不同的类型。
- 但是同管道类似,它有一个不足就是每个消息的最大长度是有上限的(MSGMAX),每个消息队列的总的字节数(MSGMNB),系统上消息队列的总数上限(MSGMNI)。可以用cat /proc/sys/kernel/msgmax查看具体的数据。
特点:
- 消息队列是面向记录的,其中的消息具有特定的格式以及优先级
- 消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除(生命周期随内核,消息队列会一直存在,需要我们显示的调用接口删除或使用命令删除)。
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
- 克服了管道只能承载无格式字节流的缺点
- 消息队列可以双向通信
对于消息队列,要知道如何创建一个消息队列,如何将消息添加到消息队列,如何从消息队列读取信息
ftok()函数:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);fname就是你指定的文件名(已经存在的文件名),一般使用当前目录,如:
key_t key;
key = ftok(".", 1); 这样就是将fname设为当前目录。
id是子序号。虽然是int类型,但是只使用8bits(1-255)。
在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。
当删除重建文件后,索引节点号由操作系统根据当时文件系统的使用情况分配,因此与原来不同,所以得到的索引节点号也不同。调用成功返回一个key值,用于创建消息队列,如果失败,返回-1键值和消息队列标识符的关系:
在创建一个消息队列(其他ipc相同)时,需要先通过文件路径名和项目ID获取一个键值,
然后通过此
键值由内核生成标识符,在以后可通过此标识符来使用此消息队列。
查询文件索引节点号的方法是:
ls -i
ls -al//显示所有文件的所以值,包括隐藏文件
执行结果:
fhn@ubuntu:~/jincheng/communication$ ls -i
932272 fifo 932283 msgread 932262 msgsend.c 932274 read 932250 write.c
932256 fifo.c 932263 msgread.c 932265 nomamepipe 932279 read.c 932267 znomamepipe
932229 fifodir 932280 msgsend 932236 nomamepipe.c 932275 write 932248 znomamepipe.c
msgget函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflag);功能:创建和访问一个消息队列
key:某个消息队列的名字,用ftok()产生
msgflag:有两个选项IPC_CREAT和IPC_EXCL,单独使用IPC_CREAT,如果消息队列不存在则创建之,
如果存在则打开返回;单独使用IPC_EXCL是没有意义的;两个同时使用,如果消息队列不存在则创建之,
如果存在则出错返回。如果将key值设为IPC_PRIVATE则创建私有的消息队列,只能有一个进程访问。
msgflag由九个权限标志构成,如0644,它们的用法和创建文件时使用的mode模式标志是一样的(但是消息队列没有x(执行)权限)返回值:成功返回一个非负整数,即消息队列的标识码,失败返回-1
为什么要有键值和标识符两个值呢?
标识符是对于用户操作而言的,让用户感觉操作和对文件的操作相同,键是对于系统内部说的。
我们使用ftok来创建键值,具体你可以man一下fotk函数,大概是这样的:按给定的路径名取得其stat结构,从该结构中取出部分st_dev和st_ino字段,然后再与项目id组合起来,如果两个路径名引用两个不同的文件,那么,对这两个路径名调用ftok通常返回不同的键,但是,因为i节点号和键通常都存放在长整型中,于是创建键时可能会丢失信息,这意味着,如果使用同一项目id,那么对于不同文件的两个路径名可能产生相同的键。而标识符是唯一确定的,可以用来区别于其他ipc的。
msgsnd()函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);功能:把一条消息添加到消息队列中
参数:
msgid:由msgget函数返回的消息队列标识码
msgp:指针指向准备发送的消息
msgze:msgp指向的消息的长度(不包括消息类型的long int长整型)
msgflg:默认为0
返回值:成功返回0,失败返回-1消息结构一方面必须小于系统规定的上限,另一方面必须以一个long int长整型开始,接受者以此来确定消息的类型struct msgbuf {long mtype; /* message type, must be > 0 */char mtext[1]; /* message data */};消息队列内一个节点类型如下:
struct msq_Node
{ Type msq_type; //类型 Length msg_len; //长度 Data msg_data; //数据 struct msg_Node *next;
};
msgrcv()函数:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgid:由msgget函数返回的消息队列标识码
msgp:指针指向准备接收的消息
msgze:msgp指向的要存消息内存的长度(不包括消息类型的long int长整型)
msgtyp:msgtyp==0返回队列中第一个消息msgtyp>0返回队列中消息类型为msgtyp的第一个消息msgtyp<0返回队列中消息类型小于或等于msgtyp绝对值的消息,如果有多个,则取类型值最小的消息。
可以看出msgtyp值非0时用以非先进先出次序读消息,也可以把msgtyp看做优先级的权值。
msgflg:默认为0,阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待
功能:是从一个消息队列接受消息
返回值:成功返回实际放到接收缓冲区里去的字符个数,失败返回-1
msgctl()函数:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);功能:消息队列的控制函数 第一个参数msgqid 是消息队列对象的标识符。
第二个参数是函数要对消息队列进行的操作,它可以是:
IPC_STAT:取出系统保存的消息队列的msqid_ds 数据,并将其存入参数buf 指向的msqid_ds 结构中。
IPC_SET:设定消息队列的msqid_ds 数据中的msg_perm 成员。设定的值由buf 指向的msqid_ds结构给出。
IPC_RMID:将队列从系统内核中删除,此时第三个参数设为NULL。
这三个命令的功能都是明显的,所以就不多解释了。唯一需要强调的是在IPC_STAT命令中队列的msqid_ds 数据中唯一能被设定的只msg_perm 成员,其是ipc_perm 类型的数据。而ipc_perm 中能被修改的只有mode,pid 和uid 成员。其他的都是只能由系统来设定的。成功返回0,失败返回-1
消息队列需要手动删除IPC资源
linux下消息队列的查看与删除(ipcs&ipcrm的使用)
消息队列接收端:
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include<string.h>
typedef struct msgbuf
{long mtye;char mtext[128];
}MSG,*PMSG;
int main()
{key_t key;int msgid;MSG readbuf;MSG sendbuf;sendbuf.mtye=666;strcpy(sendbuf.mtext,"hello sender,i have receive your msg");key=ftok(".",30);msgid=msgget(key,IPC_CREAT|0600);//flage使用IPC_CREAT表示若消息队列不存在则创建存在则打开后返回//0600是在没有消息队列时创建消息队列的权限,和文件那里那个权限一样if(msgid==-1){printf("make fail\n");perror("why");}memset(readbuf.mtext,'\0',128);msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),777,0);printf("msgrcv得到消息:%s\n",readbuf.mtext);msgsnd(msgid,&sendbuf,strlen(sendbuf.mtext),0);return 0;
}
消息队列发送端:
#include<stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include<string.h>
typedef struct msgbuf
{long mtye;char mtext[128];
}MSG,*PMSG;
int main()
{key_t key;int msgid;MSG sendbuf;MSG readbuf;sendbuf.mtye=777;strcpy(sendbuf.mtext,"hello i am sender");key=ftok(".",30);msgid=msgget(key,IPC_CREAT|0600);//flage使用IPC_CREAT表示若消息队列不存在则创建存在则打开后返回//0600是在没有消息队列时创建消息队列的权限,和文件那里那个权限一样if(msgid==-1){printf("make fail\n");perror("why");}msgsnd(msgid,&sendbuf,strlen(sendbuf.mtext),0);printf("msgsnd发送消息完成:%s\n",sendbuf.mtext);msgrcv(msgid,&readbuf,sizeof(readbuf.mtext),666,0);printf("msgsnd读到消息:%s\n",readbuf.mtext);msgctl(msgid,IPC_RMID,NULL);//等同于在命令行删除return 0;
}