进程间通信
- pipe进行父子进程间的通信
- pipe进行兄弟进程间的通信
- FIFO进行无血缘关系的进程间通信(FIFO又被称为命名管道)
- mmap函数进行有血缘无血缘进程间通信
IPC
- InerProcess Communication(进程间通信)
进程间通信的4种方式
- 管道 - 简单
- 信号 - 系统开销小
- 共享映射区 - 有无血缘关系的进程间通信都可以
- 本地套接字 - 稳定
匿名管道pipe
这里指的是匿名管道pipe
匿名管道概念
- 本质
- 内核缓冲区
- 伪文件,匿名管道在硬盘上没有这个文件,也不占用磁盘空间
- 特点
- 两部分
- 读端,写端,对应两个文件描述符
- 数据写端流入,读端流出
- 操作管道的进程被销毁之后,管道自动被释放了
- 管道默认是阻塞的
- 读写
- 两部分
匿名管道的原理
内部实现方式:队列
- 环形队列
- 特点:先进先出,数据结构特点:不用频繁移动内部元素,队头队尾会随元素移出相对移动。
缓冲区大小
- 默认4k
- 大小会根据实际情况做适当调整
匿名管道的局限性
- 队列
- 数据只能读取一次,不能重复读取
- 半双工
- 单工:遥控器
- 半双工:对讲机
- 数据传输方向是单向的
- 双工:电话
- 匿名管道pipe
- 适用于有血缘关系的进程
创建匿名管道
- int pipe(int fd[2]);
- fd 为传出参数
- fd[0]为读端
- fd[1]为写端
例:
|
|
父子进程使用匿名管道通信
练习
父子进程间通信,实现 ps aux | grep bash
- 数据重定向:dup2(oldfd,newfd)
- execlp
- 因为管道的数据只能读一次,为了防止父进程把数据写了自己读了,而且有时候会出现浪费系统资源的现象
- 父进程写:关闭读端
- 子进程读:关闭写端
- 如果需要双向通信 :那就要用两个管道了
123456789101112131415161718192021222324252627282930313233343536373839404142int main(){int fd[2];int ret = pipe(fd);if(ret == -1){perror("piper error");exit(1);}pid_t pid = fork();//fork在管道之后,这样子进程能继承父进程的文件描述符if(pid == -1){perror("fork error");exit(1);}//父进程// ps auxif(pid>0){//关闭读端close(fd[0]);//文件描述符重定向dup2(fd[1],STDOUT_FILENO);execlp("ps","ps","aux",NULL);perror("execlp");exit(1);}else if(pid == 0){close(fd[1]);dup2(fd[0],STDIN_FILENO);execlp("grep","grep","bash",NULL);}printf("pipe[0] = %d\n" fd[0]);printf("pipe[1] = %d\n" fd[1]);close(fd[0]);close(fd[1]);return 0;}
兄弟进程间通信,实现 ps aux |grep bash
- 把父进程的读和写都close掉,父进程只负责回收子进程的pcb
匿名管道的读写行为
读操作
- 有数据
- read(fd) :正常读,返回读出的字节数
- 无数据
- 写端全部关闭
- read解除阻塞,返回0
- 相对于读文件读到了尾部
- 没有全部关闭
- read阻塞
- 写端全部关闭
- 有数据
写操作
- 读端全部关闭
- 管道破裂,进程被终止
- 内核给当前进程发信号SIGPIPE
- 管道破裂,进程被终止
- 读端没全部关闭
- 缓冲区写满了
- write阻塞
- 缓冲区没有满
- write继续写
- 缓冲区写满了
- 读端全部关闭
如何设置非阻塞?
默认读写两端都阻塞
设置读端为非阻塞pipe(fd)
fcntl - 变参函数
- 修改文件属性:open的时候对应的flag属性
设置方法:
- 获取原来的flag:
int flag = fcntl(fd[0],F_GETFL);
- 生成新的flag:
flag |= NONBLOCK;
- 设置:
fcntl(fd[0],F_SETFL,flags);
- 获取原来的flag:
有名管道FIFO
特点
- 有名管道
- 在磁盘上有个文件,如果 ls -l 文件类型是 p
- 伪文件,在磁盘上大小永远为0,往里面写的数据,会被映射到内核缓冲区
- 在内核中有一个对应的缓冲区
- 半双工
使用场景
- 可以进行没有血缘关系的进程间通信
创建方式
- 命令:mkfifo 管道名
- 函数:mkfifo
FIFO文件可以使用IO函数进行操作
- open/close
- read/write
- 不能执行lseek操作
进程间通信
- 两个不相干的进程
使用
就像对待文件一样,创建,两个程序读写。
|
|
内存映射区
mmap - 创建内存映射区
|
|
addr
这个参数是建议地址(hint),没有特别需求一般设为NULL。这个函数会返回一个实际 map 的地址。len
文件长度,不能为0prot
表明对这块内存的保护方式,不可与文件访问方式冲突。PROT_READ
- 读权限,必须要有的权限
PROT_WRITE
- 写权限
PROT_EXEC
- 执行权限
使用 | 进行连接
flags
MAP_PRIVATE
- 修改了内存数据不会同步到磁盘
MAP_SHARED
- 修改内存数据会同步到磁盘。
- 与
MAP_PRIVATE
冲突。
fd
文件描述符。进行 map 之后,文件的引用计数会增加。因此,我们可以在 map 结束后关闭 fd,进程仍然可以访问它。当我们 munmap 或者结束进程,引用计数会减少。offset
- 文件偏移,从文件起始算起。必须是4K的整数倍
- 或者为 0
返回值
如果成功,返回指向映射区首地址的指针。
如果失败,mmap 函数将返回
MAP_FAILED
。
munmap 释放内存映射区
- 函数原型: int munmap(void *addr,size_t length);
- addr :mmap的返回值,映射区的首地址
- length:mmap的第二个参数,映射区的长度
|
|
使用mmap中的一些注意事项
- 如果对mmap的返回值做操作是不可以的。
- 因为munmap需要这个值进行释放操作,所以不能动。
- 需要新建一个指针等于这个指向地址,在对新建指针进行操作。
char *pt = ptr;
- open 文件的权限要大于等于 mmap创建内存映射区的权限,而且必须要有读权限。
- 可以在open的时候O_CREAT一个新文件来创建映射区么?
- 可以,但只创建没有大小不可以,需要做文件拓展
- lseek
- 或truncate(path,length);
- 可以,但只创建没有大小不可以,需要做文件拓展
- mmap后关闭文件描述符,对mmap映射有没有影响?
- 没有
- 对ptr越界操作会怎么样?
- 段错误
mmap 优势
比文件IO操作要更快,是直接在内存上处理的,不是在磁盘上,效率高,
但mmap不阻塞,用的时候要考虑读写先后顺序。
mmap 创建匿名映射区
|
|
对于有血缘关系的进程,用mmap创建匿名文件映射区,不用管磁盘上到底有没有这个文件,更方便。
mmap对于没有血缘关系进程通信
对于没有血缘关系的进程,需要借助磁盘文件创建映射区
- 不阻塞
- a.c
- 打开文件用mmap对映射区进行读写操作
- b.c
- 打开文件用mmapd对映射区做读写操作
|
|
另一个程序
|
|
补充:dup和dup2
复制文件描述符
dup
- int dup(int oldfd);
- oldfd:要复制的文件描述符
- 返回值:新的文件描述符
- dup调用成功
- 有两个文件描述符指向同一个文件
- 返回值:取最小的且没被暂用的文件描述符
- 复制完有两个文件描述符,close掉其中一个,文件没有被关闭,还有一个文件描述符。
- int dup2(int oldfd, int newfd);
- oldfd ->hello
- newfd ->world
- 假设newfd已经指向了一个文件,首先断开close与那个文件的链接,newfd指向oldfd指向的文件