进程间通信
一个管道文件的通信半双工的,且不能用lseek()
函数操作,其他操作与普通文件一样。
1、匿名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
2、命名pipe
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- 功能:
命名管道的创建。 - 参数:
pathname : 普通的路径名,也就是创建后 FIFO 的名字。
mode : 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同。(0644) - 返回值:
成功:0 状态码
失败:如果文件已经存在,则会出错且返回 -1。 - 注意:
管道默认大小65536字节
管道读写四种情况
- 写端文件描述符全部关闭 (管道写端引用计数为0) ,管道没数据,read会返回0,就像读到文件末尾一样,不设置
errno
。 - 写端没关闭,管道没数据,则
read
阻塞。若读端设置非阻塞,read
返回-1,errno
设置为EAGAIN - 读端都关闭,写端进程会收到信号
SIGPIPE
,进程异常终止。 - 读端没关闭,管道写满,写端阻塞。若写端设置非阻塞,
write
返回-1,errno
设置为EAGAIN
3、内存映射: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宏
#include <sys/mman.h>
int munmap(void *addr, size_t length);
- 功能:
释放内存映射区 - 参数:
addr:使用mmap函数创建的映射区的首地址
length:映射区的大小 - 返回值:
成功:0
失败:-1
多个进程通过对同一个文件映射达到共享内存的目的,对共享内存的操作需要配合信号量等同步机制。
注意事项
- 创建映射区的过程中,隐含着一次对映射文件的读操作。
- 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
- 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。
- 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小。mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
- munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
- mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
4、进程同步(信号量)
访问这种资源的代码称为临界区,需要一种手段(同步原语)让进程之间相互通气以便互斥访问临界区。
信号量
信号通常是系统用来通知进程,当一个信号量为0,说明没有唤醒进程的操作,如果大于0表示有多个唤醒操作。对信号量有两个操作:down(P : proberen 尝试)和up(V : verhogen 增加),分别为消耗一个信号和增加一个信号。down和up为原子操作。
down:检查信号量如果大于0,则信号量减一,否则阻塞。
up:如果有进程在这个信号量上阻塞(信号量当然为0),则唤醒一个进程,否则信号量加一。
互斥量
最大值为1的信号量。
条件变量
条件变量要配合一个互斥锁,是一个全局变量,作用是使线程阻塞直到等待一个条件成立再继续运行。条件变量是一个队列,线程检测到条件不满足会将自己加入(wait()
)这个队列里等待被唤醒。另外的线程在改变条件的时候也可以唤醒(signal()
)队列中的一个或全部线程。注意这个唤醒操作是将线程从条件变量的队列里出列,进入互斥锁的阻塞队列竞争互斥锁,而不是唤醒就直接开始执行下边的代码。没有被唤醒的线程会继续在条件变量的队列中,即使互斥锁被释放也不会去获得锁。
一个典型的条件变量使用:
wait端:
std::mutex mtx;
std::condition_variable_any cond;
void child() {
mtx.lock();
while(not some condiiton) //使用while为了避免“虚假唤醒”
cond.wait(mtx); // 释放锁,加入条件变量的阻塞队列
//被唤醒,获得锁,继续执行
// do somthing
mtx.unlock();
}
void parent() {
mtx.lock();
//do somthing,条件满足
cond.notify_one(); // 唤醒一个线程
cond.notify_all(); // 唤醒全部线程
mtx.unlock();
}
5、消息队列
消息队列是一个消息的链表,保存在内核中。每个消息队列有唯一的 key。
消息队列允许一个或多个进程向它写入与读取消息。消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。也就是说,消息队列是异步的,但这也造成了一个缺点,就是接收者必须轮询消息队列,才能收到最近的消息。