进程间通信的本质就是让不同的进程看到同一份资源,也就是同一块内存
进程间通信的目的
- 数据传输
- 资源共享
- 通知事件
- 进程控制
进程通信分离
- 管道
- 匿名管道pipe
- 命名管道
- System V IPC
- 消息队列
- 共享内存
- 信号量
- POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道
管道都是单向传输内容的
匿名管道
父进程写入,子进程读
#include <unistd.h>
int pipe(int fd[2]);
// 创建一个无名管道,fd[0]表示读端,fd[1]表示写端
// 成功返回0,失败返回错误代码
父进程创建管道,然后fork出子进程,父进程关闭fd[0],子进程关闭fd[1]。
管道的特点:
- 管道是用来进行具有血缘关系的进程进行进程间通信
- 管道具有通过进程间协同,提供了访问控制
- 管道提供的是面向流的通信服务
- 管道是基于文件的,文件的生命周期是随进程的,管道的生命周期也是随进程的
- 管道是单向通信
访问控制:
- 写快读慢,写满了就不能再写
- 写慢读块,管道没有数据的时候,读需要等待
- 写关,没有读,就读到文件结尾
- 读关,写继续,OS终止写进程
例子:从键盘读取数据,写入管道,读取管道,写到屏幕
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main( void )
{
int fds[2];
char buf[100];
int len;
if ( pipe(fds) == -1 )
perror("make pipe"),exit(1);
// read from stdin
while ( fgets(buf, 100, stdin) )
{
len = strlen(buf);
// write into pipe
if ( write(fds[1], buf, len) != len ) {
perror("write to pipe");
break;
}
memset(buf, 0x00, sizeof(buf));
// read from pipe
if ( (len=read(fds[0], buf, 100)) == -1 ) {
perror("read from pipe");
break;
}
// write to stdout
if ( write(1, buf, len) != len ) {
perror("write to stdout");
break;
}
}
}
命名管道
在不相关的进程之间交换数据,可以使用FIFO文件。命名管道就是一种特殊的文件
// 创建命名管道
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
// 函数原型:int mkfifo(const char *filename,mode_t mode);
区别
唯一的区别就是在创建与打开的方式不同。命名管道由mkfifo创建,open打开;匿名管道由pipe创建并打开。
共享内存
提供者是操作系统,共享内存=共享内存块+对应的共享内存的内核数据结构来管理
当进程结束时,共享内存块还在。system v的ipc资源,生命周期随内核,需要自己删除
ipcs -m // 查看共享内存
ipcrm -m id // 删除id的共享内存
共享是属于用户空间,就是不用经过系统调用,直接可以访问。双方进程如果想通信,直接机械能内存级的读写就可。共享内存是最快的,因为不需要拷贝,也就不用交给操作系统;但是缺乏访问控制,会带来并发问题。
信号量
让不同的进程看到了同一份资源,也带来了一些时序问题,造成数据不一致问题
临界资源:多个进程(执行流)看到的公共的一份资源
临界区:自己的进程,访问临界资源的代码
互斥:为了更好的进行临界区的保护,可以让多执行流在任何时刻都只能一个进程进入临界区
原子系:要么不做,要么做完,没有中间状态
所以多个执行流,运行的时候互相干扰,主要是我们不加保护的访问了同样的资源(临界资源),在非临界区多个执行流互相是不影响的
申请信号量的本质:让信号量计数器--,P操作,必须是原子的
申请信号量成功,临界资源内部,一定会给你预留你想要的资源,本质就是临界资源的一种预定机制。
释放信号量就是计数器++,V操作,必须是原子的