进程间通信
pipe-父子、兄弟进程间通信 fifo-无血缘关系的进程间通信 mmap
# 进程间通信相关概念
- IPC (InterProcess Communication)
- 进程间通信常用的4中实现方式
- 管道 - 简单
- 信号 - 系统开销小
- 共享映射区 - 有无血缘关系的进程间通信都可以
- 本地套接字 - 稳定
# 管道
管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
- 其本质是一个伪文件(实为内核缓冲区)
- 由两个文件描述符引用,一个表示读端,一个表示写端。
- 规定数据从管道的写端流入管道,从读端流出;操作管道的进程被销毁之后,管道自动被释放;管道默认是阻塞的,因此不需要使用sleep函数。 管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。 管道的局限性: ① 数据一旦被读走,便不在管道中存在,不可反复读取。 ② 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。 ③ 只能在有血缘关系的进程间使用管道。 常见的通信方式有,单工通信、半双工通信、全双工通信。
# pipe函数
创建管道
//成功:0;失败:-1,设置errno
//pipefd[0]-读端 pipefd[1]-写端
//pipefd无需open,但需手动close。
int pipe(int pipefd[2]);
int main()
{
pid_t pid;
char buf[1024];
int fd[2];
char *p = "test for pipe\n";
int ret = pipe(fd);
if(ret == -1){
perror("pipe error\n");
exit(1);
}
pid_t pid = fork();
if(pid<0){
perror("fork error\n");
exit(1);
}
if (pid == 0) {
close(fd[1]);
int len = read(fd[0], buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
close(fd[0]);
} else {
close(fd[0]);
write(fd[1], p, strlen(p));
wait(NULL);
close(fd[1]);
}
// if (pid == 0) { //child
// close(fd[1]); //子进程从管道中读数据,关闭写端
// dup2(fd[0], STDIN_FILENO); //让wc从管道中读取数据
// execlp("wc", "wc", "-l", NULL); //wc命令默认从标准读入取数据
// } else {
// close(fd[0]); //父进程向管道中写数据,关闭读端
// dup2(fd[1], STDOUT_FILENO); //将ls的结果写入管道中
// execlp("ls", "ls", NULL); //ls输出结果默认对应屏幕
// }
printf("pipe[0] = %d\n", fd[0]);
printf("pipe[1] = %d\n", fd[1]);
close(fd[0]);
close(fd[1]);
return 0;
}
dup2函数 原型:int dup2(int oldfd, int newfd); 参数:oldfd是一个文件描述符,newfd一个文件描述符 功能:将newfd指向oldfd所指的文件,相当于重定向功能。如果newfd已经指向一个已经打开的文件,那么他会首先关闭这个文件,然后在使newfd指向oldfd文件;如果newfd和oldfd指向同一个文件,那么不会关闭,直接返回。
# 管道的读写行为
① 读管道:
- 管道中有数据,read返回实际读到的字节数。
- 管道中无数据: (1) 管道写端被全部关闭,read返回0 (好像读到文件结尾) (2) 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu) ② 写管道:
- 管道读端全部被关闭,管道破裂,进程异常终止(内核给当前进程发SIGPIPE信号)
- 管道读端没有全部关闭: (1) 管道已满,write阻塞。 (2) 管道未满,write将数据写入,并返回实际写入的字节数。 ③ 如何设置非阻塞?
- 默认读写两端都阻塞
- 设置读端为非阻塞pipe
//获取原来的flags
int flags = fcntl(fd[0], F_GETFL);
//设置新的flags
flags |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);
# fifo有名管道
FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。
# 创建方式:
- 命令:mkfifo 管道名
- 库函数:int mkfifo(const char *pathname, mode_t mode); 成功:0; 失败:-1 一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
int fd, len;
char buf[4096];
if (argc < 2) {
printf("./a.out fifoname\n");
return -1;
}
fd = open(argv[1], O_RDONLY);
if (fd < 0)
sys_err("open");
while (1) {
len =read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
sleep(1); //多個读端时应增加睡眠秒数,放大效果.
}
close(fd);
return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(int argc, char *argv[])
{
int fd, i;
char buf[4096];
if (argc < 2) {
printf("Enter like this: ./a.out fifoname\n");
return -1;
}
/*
int ret = access("myfifo", F_OK);
if (ret != 0) {
mkfifo("myfifo", 0664);
}
*/
fd = open(argv[1], O_WRONLY);
if (fd < 0)
sys_err("open");
i = 0;
while (1) {
sprintf(buf, "hello itcast %d\n", i++);
write(fd, buf, strlen(buf));
sleep(2);
}
close(fd);
return 0;
}
# mmap 共享存储映射
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。 使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
# mmap函数-创建内存映射
将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件
// 返回:成功:返回创建的映射区首地址;失败:MAP_FAILED宏
// 参数:
// addr: 建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL
// length: 欲创建映射区的大小
// prot: 映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
// flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
// MAP_SHARED: 会将映射区所做的操作反映到物理设备(磁盘)上。
// MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
// fd: 用来建立映射区的文件描述符
// offset: 映射文件的偏移(4k的整数倍)
void *mmap(void *adrr, size_t length, int prot, int flags, int fd, off_t offset);
# munmap函数 释放内存映射
同malloc函数申请内存空间类似的,mmap建立的映射区在使用结束后也应调用类似free的函数来释放。
//成功:0; 失败:-1
int munmap(void *addr, size_t length);
借鉴malloc和free函数原型,尝试装自定义函数smalloc,sfree来完成映射区的建立和释放。思考函数接口该如何设计?
使用mmap当前磁盘文件的内容
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
int main()
{
//打开一个文件
int fd = open("english.txt", O_RDWR);
int len = lseek(fd, 0, SEEK_END);
//创建内存映射区
void* ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED){
perror("map error");
exit(1);
}
printf("%s", (char*)ptr);
//释放内存映射区
munmap(ptr, len);
close(fd);
return 0;
}
# 创建匿名映射区 - 有血缘关系
//使用MAP_ANONYMOUS (或MAP_ANON), 如:
//-1表示不用fd,匿名映射
int *p = mmap(NULL, 4094, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
// 需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
// ① fd = open("/dev/zero", O_RDWR);
// ② p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);