最近在学习 APUE,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习APUE的童鞋们提供一份参考。
本系列博文均根据学习《UNIX环境高级编程》一书总结而来;
运行环境:
- 操作系统: ubutnu 16.04
- 编译器:QtCreator CLion 2020.3
进程间通信
进程间通信的基本大概可以理解为:两个进程要想完成数据交换,必须通过内核,一个进程将输入写入内核,另一个进程从内核读走数据;
pipe
管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道。
管道的本质是一块内核缓冲区
由两个文件描述符引用,一个表示读端,一个表示写端。
规定数据从管道的写端流入管道,从读端流出。
当两个进程都终结的时候,管道也自动消失。
管道的读端和写端默认都是阻塞的。
管道原理
管道的实质是内核缓冲区,内部使用环形队列实现。
默认缓冲区大小为4K,可以使用ulimit -a命令获取大小。
实际操作过程中缓冲区会根据数据压力做适当调整。
数据一旦被读走,便不在管道中存在,不可重复读数据
数据只能在一个方向流动,若要实现双向管道,必须使用两个管道
只能在有学院关系的进程间使用
管道使用
一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在血缘关系,这里的血缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。父子进程间具有相同的文件描述符,且指向同一个管道**pipe**,其他没有关系的进程不能获得pipe()产生的两个文件描述符,也就不能利用同一个管道进行通信。
父进程创建管道
父进程fork出子进程
父进程关闭fd[0],子进程关闭fd[1]
1 | /** |
Test:
1 | /** |
管道读写行为
读操作
有数据
- read正常读,返回读出的字节数
无数据
写端全部关闭
- read解除阻塞,返回0, 相当于读文件读到了尾部
没有全部关闭
- read阻塞
写操作
读端全部关闭
- 管道破裂,进程终止, 内核给当前进程发SIGPIPE信号
读端没全部关闭
缓冲区写满了
- write阻塞
缓冲区没有满
- 继续write
管道阻塞
默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参
考下列三个步骤进行:
1 | 第1步: int flags = fcntl(fd[0], F_GETFL, 0); |
若是读端设置为非阻塞:
写端没有关闭,管道中没有数据可读,则read返回-1;
写端没有关闭,管道中有数据可读,则read返回实际读到的字节数
写端已经关闭,管道中有数据可读,则read返回实际读到的字节数
写端已经关闭,管道中没有数据可读,则read返回0
查看管道缓冲区大小
1 | ulimit -a |
FIFO
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间通信。但通过FIFO,不相关的进程也能交换数据。
FIFO是Linux基础文件类型中的一种(文件类型为p,可通过ls -l查看文件类型)。但FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来标识内核中一条通道。进程可以打开这个文件进行read/write,实际上是在读写内核缓冲区,这样就实现了进程间通信。
创建FIFO
方式1-使用命令 mkfifo
命令格式: mkfifo 管道名
例如:mkfifo myfifo
方式2-使用函数
1 | int mkfifo(const char *pathname, mode_t mode); |
参数说明和返回值可以查看man 3 mkfifo
当创建了一个FIFO,就可以使用open函数打开它,常见的文件I/O函数都可用于FIFO。如:close、read、write、unlink等。
FIFO严格遵循先进先出(first in first out),对FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek**()**等文件定位操作。
使用FIFO完成两个进程通信
使用FIFO完成两个进程通信的示意图
进程A:
- 创建一个FIFO文件:mkfifo命令或使用mkfifo函数
- open file文件,获得一个文件描述符fd
- 写fifo文件——write
- 关闭fifo文件—-close
进程B:
- 打开fifo文件,获得文件描述符fd
- 读fifo文件——read
- 关闭fifo文件—-clsoe
access
1 | int access(const char* pathname, int mode); |
内存映射区
简介
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
mmap
1 |
|
addr: 指定映射的起始地址, 通常设为NULL, 由系统指定
length:映射到内存的文件长度
prot: 映射区的保护方式, 最常用的:
读:PROT_READ
写:PROT_WRITE
读写:PROT_READ | PROT_WRITE
flags: 映射区的特性, 可以是
MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享。
MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。
fd:由open返回的文件描述符, 代表要映射的文件。
offset:以文件开始处的偏移量, 必须是**4k**的整数倍, 通常为0, 表示从文件头开始映射。
匿名映射
1 | /** |
munmap
1 | int munmap(void *addr, size_t length); |
注意
创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区
当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。
特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
文件偏移量必须为0或者4K的整数倍
mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。