一、通信方式
Linux 操作系统支持的主要进程间通信的通信机制:
二、管道
2.1 无名管道
管道也叫无名管道,它是是 UNIX 系统 IPC(进程间通信) 的最古老形式,所有的 UNIX 系统都支持这种通信机制。
管道有如下特点:
-
- 半双工,数据在同一时刻只能在一个方向上流动。
-
- 数据只能从管道的一端写入,从另一端读出。
-
- 写入管道中的数据遵循先入先出的规则。
-
- 管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。
-
- 管道不是普通的文件,不属于某个文件系统,其只存在于内存中。
-
- 管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
-
- 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。
-
- 管道没有名字,只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
对于管道特点的理解,我们可以类比现实生活中管子,管子的一端塞东西,管子的另一端取东西。
管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。
2.1.2 无名管道在控制台中执行:
ps aux | grep l
2.2.2 无名管道在程序中执行:
int pipe(int fd[2])
这里表示创建一个匿名管道,并返回了两个描述符,一个是管道的读取端描述符 fd[0],另一个是管道的写入端描述符 fd[1]。注意,这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中
这样操作会在一个进程中创建一个管道
其实,所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。
我们可以使用 fork 创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个「 fd[0] 与 fd[1]」,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。
管道只能一端写入,另一端读出,所以上面这种模式容易造成混乱,因为父进程和子进程都可以同时写入,也都可以读出。那么,为了避免这种情况,通常的做法是:
- 父进程关闭读取的 fd[0],只保留写入的 fd[1];
- 子进程关闭写入的 fd[1],只保留读取的 fd[0];
所以说如果需要双向通信,则应该创建两个管道。
pipe()
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建无名管道。
参数:
pipefd : 为 int 型数组的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。
当一个管道建立时,它会创建两个文件描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道。一般文件 I/O的函数都可以用来操作管道(lseek() 除外)。
返回值:
成功:0
失败:-1
实例:
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#define size 64
int main()
{
int fd_pipe[2] = { 0 };//1:write;0:read
pid_t pid;
if (pipe(fd_pipe) < 0)
{// 创建管道
perror("pipe");
}
pid = fork(); // 创建
if (pid == 0)
{ // 子进程
close(fd_pipe[0]);
char buf[] = "I am mike";
// 往管道写端写数据
printf("i am son and i am writing ...\n");
write(fd_pipe[1], buf, strlen(buf));
exit(0);
}
else if (pid > 0)
{// 父进程
close(fd_pipe[1]);
wait(NULL); // 等待子进程结束,回收其资源
char str[50] = { 0 };
// 从管道里读数据
printf("i father and i am reading ...\n");
read(fd_pipe[0], str, sizeof(str));
printf("str=[%s]\n", str); // 打印数据
}
return 0;
}
结果:
2.2.3 查看管道缓冲区
#include <unistd.h>
long fpathconf(int fd, int name);
功能:该函数可以通过name参数查看不同的属性值
参数:
fd:文件描述符
name:
_PC_PIPE_BUF,查看管道缓冲区大小
_PC_NAME_MAX,文件名字字节数的上限
返回值:
成功:根据name返回的值的意义也不同。
失败: -1
实例:
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#define size 64
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret == -1)
{
perror("pipe error");
exit(1);
}
long num = fpathconf(fd[0], _PC_PIPE_BUF);
printf("num = %ld\n", num);
return 0;
}
结果:
2.2.4 无名管道读写特点
读管道:
-
管道中有数据,read返回实际读到的字节数。
-
管道中无数据:
-
- 管道写端被全部关闭,read返回0 (相当于读到文件结尾)
-
- 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
写管道:
-
管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程终止)
-
管道读端没有全部关闭:
-
- 管道已满,write阻塞。
-
- 管道未满,write将数据写入,并返回实际写入的字节数。
可以看出会有阻塞的情况发生,使用以下方法可以避免阻塞:
设置为非阻塞的方法
//获取原来的flags
int flags = fcntl(fd[0], F_GETFL);
// 设置新的flags
flag |= O_NONBLOCK;
// flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);
fcntl()详解:
#include<unistd.h>
#include<fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd ,struct flock* lock);
int fcntl(int fd, int cmd, ... /* arg */ );
(经常用这个fcntl函数改变非阻塞)
参数:
fd:文件描述符
cmd:设置的命令
F_GETFL(常用)
F_SETFL(常用)
arg:可有可无,由第二个参数决定,比如get时候没有,set时候有值
返回值:
文件状态标志
-1 :失败
步骤:
int flag;
flag = fcntl(sockfd, F_GETFL, 0);
flag |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, flag);
若你想设置回阻塞,只需要把Flags改为iFlags&~O_NONBLOCK即可。
函数传入值cmd
F_DUPFD:复制一个现存的描述符
F_GETFD:获得fd的close-on-exec(执行时关闭)文件描述符标志,若标志未设置,则文件经过exec()函数之后仍保持打开状态
F_SETFD:设置close-on-exec 标志,该标志由参数arg 的FD_CLOEXEC位决定
F_GETFL:得到open设置的标志
F_SETFL :改变open设置的标志
F_GETLK:根据lock参数值,决定是否可以上文件锁
F_SETLK:设置lock参数值的文件锁
2.2 有名管道
管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道(FIFO),也叫有名管道、FIFO文件。
命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
命名管道(FIFO)和无名管道(pipe)有一些特点是相同的,不一样的地方在于:
-
FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中。
-
当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
-
FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。
2.2.1 控制台使用有名管道
创建管道:
mkfifo myPipe
输出数据到管道:
echo "hello" > myPipe
在管道另一边接收结果:
cat < myPipe
结果:
2.2.3 有名管道在程序中执行
myfifo()
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
功能:
命名管道的创建。
参数:
pathname : 普通的路径名,也就是创建后 FIFO 的名字。
mode : 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同。(0666)
返回值:
成功:0 状态码
失败:如果文件已经存在,则会出错且返回 -1。
操作示范:
//进行1,写操作
int fd = open("my_fifo", O_WRONLY);
char send[100] = "Hello Mike";
write(fd, send, strlen(send));
//进程2,读操作
int fd = open("my_fifo", O_RDONLY);//等着只写
char recv[100] = { 0 };
//读数据,命名管道没数据时会阻塞,有数据时就取出来
read(fd, recv, sizeof(recv));
printf("read from my_fifo buf=[%s]\n", recv);
实例(使用两个管道实现聊天功能):
补充:fgets()
char *fgets(char *str, int n, FILE *stream);
str:这是指向一个字符数组的指针,该数组存储了要读取的字符串。
n:这是要读取的最大字符数(包括最后的空字符)。通常是使用以 str 传递的数组长度。
stream:这是指向 FILE 对象的指针,该 FILE 对象标识了要从中读取字符的流。
返回值:如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。如果发生错误,返回一个空指针。
在读字符时遇到end-of-file,则eof指示器被设置,如果还没读入任何字符就遇到这种情况,则stream保持原来的内容,返回NULL;如果发生读入错误,error指示器被设置,返回NULL,stream的值可能被改变。
实例:
用户A:(先读后写)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fdw = -1;
int fdr = -1;
int ret = -1;
fdr = open("fifo1",O_RDONLY);
fdw = open("fifo2",O_WRONLY);
char buf[64]={0};
printf("chat start!\n");
while(1)
{
memset(buf,0,64);
ret = read(fdr,buf,64);
if(ret = -1);
perror("read");
printf("read:%s,\n",buf);
memset(buf,0,64);
printf("i am writing:\n");
fgets(buf,64,stdin);
if('\n' == buf[strlen(buf)-1])
buf[strlen(buf)-1] = '\0';
write(fdw,buf,strlen(buf));
}
return 0;
}
用户B:(先写后读)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fdw = -1;
int fdr = -1;
int ret = -1;
fdw = open("fifo1",O_WRONLY);
fdr = open("fifo2",O_RDONLY);
char buf[64]={0};
printf("chat start!\n");
while(1)
{
memset(buf,0,64);
printf("i am writing:\n");
fgets(buf,64,stdin);
if('\n' == buf[strlen(buf)-1])
buf[strlen(buf)-1] = '\0';
write(fdw,buf,strlen(buf));
memset(buf,0,64);
ret = read(fdr,buf,64);
if(ret = -1)
perror("read");
printf("read:%s,\n",buf);
}
return 0;
}
结果:
三、消息队列-管道改进
前面说到管道的通信方式是效率低的,因此管道不适合进程间频繁地交换数据。
对于这个问题,消息队列的通信模式就可以解决。比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。
再来,消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。
消息这种模型,两个进程之间的通信就像平时发邮件一样,你来一封,我回一封,可以频繁沟通了。
但邮件的通信方式存在不足的地方有两点,一是通信不及时,二是附件也有大小限制,这同样也是消息队列通信不足的点。
消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAX 和 MSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
#共享内存
四、共享存储映射-避免用户态与内核态之间的数据拷贝开销
4.1 共享内存概念
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度
于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式, 因为进程可以直接读写内存,而不需要任何数据的拷贝。
4.2 存储映射函数
(1) mmap函数
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:
将一个文件或者其它对象映射进内存
参数:
addr : 指定映射的起始地址, 通常设为NULL, 由系统指定
length:映射到内存的文件长度
prot: 映射区的保护方式, 最常用的 :
a) 读:PROT_READ
b) 写:PROT_WRITE
c) 读写:PROT_READ | PROT_WRITE
flags: 映射区的特性, 可以是
a) MAP_SHARED : 写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。
b) MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write), 对此区域所做的修改不会写回原文件。
fd:由open返回的文件描述符, 代表要映射的文件。
offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射
返回值:
成功:返回创建的映射区首地址
失败:MAP_FAILED宏
关于mmap函数的使用总结:
-
- 第一个参数写成NULL
-
- 第二个参数要映射的文件大小 > 0
-
- 第三个参数:PROT_READ 、PROT_WRITE
-
- 第四个参数:MAP_SHARED 或者 MAP_PRIVATE
-
- 第五个参数:打开的文件对应的文件描述符
-
- 第六个参数:4k的整数倍,通常为0
(2) munmap函数
#include <sys/mman.h>
int munmap(void *addr, size_t length);
功能:
释放内存映射区
参数:
addr:使用mmap函数创建的映射区的首地址
length:映射区的大小
返回值:
成功:0
失败:-1
使用示例:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/mman.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
//以读写方式打开一个文件(此文件用于映射)
int fd = -1;
int ret = -1;
void *addr = NULL;
fd = open("txt",O_RDWR);
if(-1 == fd)
{
perror("open");
return 1;
}
//将文件映射到内存中
addr = mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 1;
}
printf("映射成功!");
//关闭文件
close(fd);
//写映射区
memcpy(addr,"123456789", 9);
//断开映射区
munmap(addr,1024);
return 0;
}
结果:
txt:
执行:
txt:
4.3 父子进程使用存储映射通信
实例:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/mman.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
//以读写方式打开一个文件(此文件用于映射)
int fd = -1;
int ret = -1;
void *addr = NULL;
pid_t pid = -1;
fd = open("txt",O_RDWR);
if(-1 == fd)
{
perror("open");
return 1;
}
//将文件映射到内存中
addr = mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 1;
}
//printf("映射成功!\n");
//关闭文件
close(fd);
pid = fork();
if(-1 == pid)
{
perror("pid");
return 1;
}
if(0 == pid )
{
memcpy(addr,"123456789", 9);
}else
{
wait(NULL);
printf("addr:%s\n",(char *)addr);
}
//断开映射区
munmap(addr,1024);
return 0;
}
结果:
4.4 不同进程使用存储映射通信
实例:
进程a:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/mman.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
//以读写方式打开一个文件(此文件用于映射)
int fd = -1;
int ret = -1;
void *addr = NULL;
fd = open("txt",O_RDWR);
if(-1 == fd)
{
perror("open");
return 1;
}
//将文件映射到内存中
addr = mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 1;
}
printf("映射成功!");
//关闭文件
close(fd);
//读映射区
printf("addr:%s",(char *)addr);
//断开映射区
munmap(addr,1024);
return 0;
}
进程b:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/mman.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
//以读写方式打开一个文件(此文件用于映射)
int fd = -1;
int ret = -1;
void *addr = NULL;
fd = open("txt",O_RDWR);
if(-1 == fd)
{
perror("open");
return 1;
}
//将文件映射到内存中
addr = mmap(NULL,1024,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 1;
}
printf("映射成功!");
//关闭文件
close(fd);
//写映射区
memcpy(addr,"123456789", 9);
//断开映射区
munmap(addr,1024);
return 0;
}
结果:
4.5 使用匿名进程实现父子进程通信-父子通信不依赖文件
使用MAP_ANONYMOUS (或MAP_ANON)。
int *p = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
1024:该位置表示映射区大小,可依实际需要填写。
MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。
在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
实例:
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/mman.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
// 创建匿名内存映射区
int len = 4096;
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
if (ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
// 创建子进程
pid_t pid = fork();
if (pid > 0) //父进程
{
// 写数据
strcpy((char*)ptr, "hello mike!!");
// 回收
wait(NULL);
}
else if (pid == 0)//子进程
{
sleep(1);
// 读数据
printf("%s\n", (char*)ptr);
}
// 释放内存映射区
int ret = munmap(ptr, len);
if (ret == -1)
{
perror("munmap error");
exit(1);
}
return 0;
}
结果: