一、什么是进程间通信?
Linux环境下,进程地址空间是相互独立的,每个进程有各自独立的用户地址空间,进程之间的全局变量在另一个进程中都看不到,要交换数据必须通过内核。进程1把数据写入内核的一个缓冲区,另一个进程可以从内核缓冲区读走,内核提供的这种机制就是进程间通信IPC(InterProcess Communication)。
二、常用进程间通信方式
1. 管道-pipe
①管道也称匿名管道,应用于有血缘关系的进程通信。
管道特质:
(1) 管道本质是一块内核缓冲区。
(2) 由两个文件描述符引用,一个读端,一个写端。
(3) 数据从写端流入,读端流出。
(4) 当两个进程都终结的时候,管道也自动消失。
(5) 管道的读端和写端都是默认阻塞的。
②管道的原理:
管道的实质是内核缓冲区,内部使用环形队列实现,默认缓冲区大小为4K,可以使用ulimit -a命令获取大小,实际操作过程中缓冲区会根据数据压力做适当调整。
③管道的局限性:
(1) 不可以反复读取,数据一旦读走便不在管道中存在。
(2) 数据只能单向流动,需要两个管道才能实现双向流动。
(3) 只能在有血缘关系的进程间使用管道。因为没有血缘关系的进程无法获取管道描述符。
④创建管道:
使用pipe函数创建管道:函数原型:
int pipe(int fd[2]);
若函数调用成功,返回0,fd[0]存放管道的读端,fd[1]存放管道的写端。
调用失败返回-1,并设置errno值。
⑤管道实例:
父子进程间通信:
(1) 父进程创建管道
(2) 父进程fork出子进程
(3) 父进程关闭fd[0],子进程关闭fd[1]
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
//创建管道
//int pipe(int pipefd[2]);
int fd[2];
int ret = pipe(fd);
if(ret<0)
{
perror("pipe error");
return -1;
}
//创建子进程
pid_t pid = fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid>0)
{
//关闭读端
close(fd[0]);
sleep(5);
write(fd[1], "hello world", strlen("hello world"));
wait(NULL);
}
else
{
//关闭写端
close(fd[1]);
char buf[64];
memset(buf, 0x00, sizeof(buf));
int n = read(fd[0], buf, sizeof(buf));
printf("read over, n==[%d], buf==[%s]\n", n, buf);
}
return 0;
}
⑥管道的读写行为:
- 读操作:
- 有数据:read正常读,返回读出的字节数
- 无数据:写端全部关闭
- read解除阻塞,返回0,相当于读文件读到了尾部
- 没有全部关闭
- read阻塞
- 写操作:
- 读端全部关闭:管道破裂,进程终止,内核给当前进程发SIGPIPE信号
- 读端没全部关闭:
- 缓冲区写满了:write阻塞
- 缓冲区没有满:继续write
⑦如何设置管道为非阻塞:
默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参
考下列三个步骤进行:
第1步: int flags = fcntl(fd[0], F_GETFL, 0);
第2步: flag |= O_NONBLOCK;
第3步: fcntl(fd[0], F_SETFL, flags);
若是读端设置为非阻塞:
-
写端没有关闭,管道中没有数据可读,则read返回-1;
-
写端没有关闭,管道中有数据可读,则read返回实际读到的字节数
-
写端已经关闭,管道中有数据可读,则read返回实际读到的字节数
-
写端已经关闭,管道中没有数据可读,则read返回0
int main() { //创建管道 //int pipe(int pipefd[2]); int fd[2]; int ret = pipe(fd); if(ret<0) { perror("pipe error"); return -1; } printf("pipe size==[%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF)); printf("pipe size==[%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF)); //关闭写端 close(fd[1]); //设置管道的读端为非阻塞 int flag = fcntl(fd[0], F_GETFL); flag |= O_NONBLOCK; fcntl(fd[0], F_SETFL, flag); char buf[64]; memset(buf, 0x00, sizeof(buf)); int n = read(fd[0], buf, sizeof(buf)); printf("read over, n==[%d], buf==[%s]\n", n, buf); return 0; }
⑧查看管道缓冲区大小:
(1) 命令:ulimit -a
(2) 函数:
long fpathconf(int fd, int name);
printf("pipe size==[%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF));
printf("pipe size==[%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF));
2. FIFO命名管道
①管道只能用于有血缘关系的进程间通信,但是FIFO可以用以不相关的进程间交换数据
FIFO是Linux基础文件类型的一种(文件类型:p)但FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来标识内核中的一条通道。进程可以打开这个文件进行read/write,实际上也是在读取内核缓冲区,进而实现进程间通信
②创建FIFO命名管道
(1) 方式1:使用命令:mkfifo
格式:mkfifo 管道名
(2) 方式2:使用函数:
int mkfifo(const char *pathname, mode_t mode);(可以使用man 3 mkfifo命令查看)
当创建了一个FIFO,就可以使用open函数打开它,进行常见的文件I/O操作,如:write、read、close、unlink等
注意:FIFO严格遵循先进先出,都FIFO的读总是从文件开头开始,写总是从末尾开始写入,他们不支持诸如lseek()的文件定位操作
③FIFO完成两个进程通信的示意图:
思路:
进程A:
创建一个fifo文件:myfifo
调用open函数打开myfifo文件
调用write函数写入一个字符串如:“hello world”(其实是将数据写入到了内核缓冲区)
调用close函数关闭myfifo文件
进程B:
调用open函数打开myfifo文件
调用read函数读取文件内容(其实就是从内核中读取数据)
打印显示读取的内容
调用close函数关闭myfifo文件
//write.c
int main()
{
//创建fifo文件
//int mkfifo(const char *pathname, mode_t mode);
int ret = access("./myfifo", F_OK);
if(ret!=0)
{
ret = mkfifo("./myfifo", 0777);
if(ret<0)
{
perror("mkfifo error");
return -1;
}
}
//打开文件
int fd = open("./myfifo", O_RDWR);
if(fd<0)
{
perror("open error");
return -1;
}
//写fifo文件
int i = 0;
char buf[64];
while(1)
{
memset(buf, 0x00, sizeof(buf));
sprintf(buf, "%d:%s", i, "hello world");
write(fd, buf, strlen(buf));
sleep(1);
i++;
}
//关闭文件
close(fd);
//getchar();
return 0;
}
//read.c
int main()
{
//创建fifo文件
//int mkfifo(const char *pathname, mode_t mode);
//判断myfofo文件是否存在,若不存在则创建
int ret = access("./myfifo", F_OK);
if(ret!=0)
{
ret = mkfifo("./myfifo", 0777);
if(ret<0)
{
perror("mkfifo error");
return -1;
}
}
//打开文件
int fd = open("./myfifo", O_RDWR);
if(fd<0)
{
perror("open error");
return -1;
}
//读fifo文件
int n;
char buf[64];
while(1)
{
memset(buf, 0x00, sizeof(buf));
n = read(fd, buf, sizeof(buf));
printf("n==[%d], buf==[%s]\n", n, buf);
}
//关闭文件
close(fd);
//getchar();
return 0;
}
3. 内存映射区
①原理:
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与内存中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作,也就避免了用户态到内核态的频繁切换,可以降低系统开销。
使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
②mmap函数
(1)函数作用:建立存储映射区
(2)函数原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
(3)函数返回值:成功返回创建的映射区首地址,失败返回MAP_FAILED宏,设置errno值
(4)参数:
- addr:指定映射区的开始地址,设为NULL则由系统指定。
- length:映射到内存的文件长度。
- prot:映射区的保护方式,最常用的:
- 读:PROT_READ
- 写:PROT_WRITE
- 读写:PROT_READ | PROT_WRITE
- flags:映射区的特性,可以是:
- MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享。
- MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制, 对此区域所做的修改不会写回原文件。
- fd:由open返回的文件描述符,代表要映射的文件。
- offset:以文件开始处的偏移量,必须是4K的整数倍,通常为0,即从文件头开始映射。
③munmap函数
- 函数作用:释放由mmap函数建立的存储映射区。
- 函数原型:int munmap(void *addr, size_t length);
- 返回值:成功返回0,失败返回-1,并设置errno值。
- 函数参数:
- addr:调用mmap函数成功返回的映射区首地址
- length:映射区大小(即:mmap函数的第二个参数)
注意事项:
(1) 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区。
(2) 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
(3) 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。
(4) 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
(5) munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
(6) 文件偏移量必须为0或者4K的整数倍.
(7) mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
④例:
-
使用mmap完成父子进程通信:
int main() { //使用mmap函数建立共享映射区 int fd = open("./test.log", O_RDWR); if(fd<0) { perror("open error"); return -1; } int len = lseek(fd, 0, SEEK_END); void * addr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); //void * addr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0); if(addr==MAP_FAILED) { perror("mmap error"); return -1; } close(fd); //创建子进程 pid_t pid = fork(); if(pid<0) { perror("fork error"); return -1; } else if(pid>0) { memcpy(addr, "hello world", strlen("hello world")); wait(NULL); } else if(pid==0) { sleep(1); char *p = (char *)addr; printf("[%s]", p); } return 0; }```
示意图:
思路:
(1)调用mmap函数创建存储映射区,返回映射区首地址addr。
(2)调用fork函数创建子进程,子进程也拥有了映射区首地址addr。
(3)父子进程可以通过映射区首地址指针addr完成通信。
(4)调用munmap函数释放存储映射区。
- 使用mmap完成没有血缘关系的进程间的通信:
思路:两个进程打开相同的文件,然后建立存储映射区,这样两个进程共享同一个存储映射区。
mmap_read.c:
{
//使用mmap函数建立共享映射区
//void *mmap(void *addr, size_t length, int prot, int flags,
// int fd, off_t offset);
int fd = open("./test.log", O_RDWR);
if(fd<0)
{
perror("open error");
return -1;
}
int len = lseek(fd, 0, SEEK_END);
//建立共享映射区
void * addr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(addr==MAP_FAILED)
{
perror("mmap error");
return -1;
}
char buf[64];
memset(buf, 0x00, sizeof(buf));
memcpy(buf, addr, 10);
printf("buf=[%s]\n", buf);
return 0;
}
mmap_write.c:
int main()
{
//使用mmap函数建立共享映射区
//void *mmap(void *addr, size_t length, int prot, int flags,
// int fd, off_t offset);
int fd = open("./test.log", O_RDWR);
if(fd<0)
{
perror("open error");
return -1;
}
int len = lseek(fd, 0, SEEK_END);
//建立共享映射区
void * addr = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(addr==MAP_FAILED)
{
perror("mmap error");
return -1;
}
memcpy(addr, "0123456789", 10);
return 0;
}
- 使用mmap函数建立匿名映射:
mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
例://使用mmap匿名映射完成父子进程间通信 int main() { //使用mmap函数建立共享映射区 void * addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); if(addr==MAP_FAILED) { perror("mmap error"); return -1; } //创建子进程 pid_t pid = fork(); if(pid<0) { perror("fork error"); return -1; } else if(pid>0) { memcpy(addr, "hello world", strlen("hello world")); wait(NULL); } else if(pid==0) { sleep(1); char *p = (char *)addr; printf("[%s]", p); } return 0; }