进程间通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程;
资源共享:多个进程之间共享同样的资源;
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(例如进程终止时要通知父进程);
进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能及时直到它的状态;
查看系统中的进程通信方式:
ipcs -m/-s/-q
删除:
ipcrm -m shmid
管道(匿名管道 / 命名管道)
管道时 Unix 中最古老的通信方式,我们把从一个进程连接到另一个进程的一个数据流叫做一个“管道”;
管道是半双工通信、本质上是内存中的一块缓冲区(多个进程访问同一块缓冲区实现数据传输通信);
管道自带同步与互斥(当对管道读写大小小于PIPE_BUF(4096),保证原子性):
原子性操作:操作不可被打断
同步:对临界资源访问的时序可控性(时序控制,一个进程操作完了,写一个进程才能操作)
互斥:临界资源的同一时唯一访问性(保护,只能有一个进程进行操作)
管道提供字节流服务,传时方式灵活,但是数据之间没有边界容易造成粘包问题,管道的声明周期和进程同步;
1、匿名管道
进程间实现通信,在内核中创建管道,操作系统返回两个描述符作为管道的操作句柄,对管道的操作就是基础的IO操作(提供两个描述符是因为管道是双向选择的);
特点:匿名管道仅能使用于具有亲缘关系的进程间通信,通过复制父进程获取管道的操作句柄;
#include<unistd.h>
int pipe(int pipefd[2]);
pipefd:用于获取管道的操作句柄
pipefd[1]:管道的读取端
pipefd[2]:管道的写入端
因为匿名管道只能用于具有亲缘关系的进程间通信:必须将管道创建在创建子进程之前(子进程通过复制父进程得到管道的操作句柄)
读写特性:
1、管道中若没有了数据,则read 会阻塞;2、若管道中的数据满了,则write 会阻塞;
3、若管道所有写段被关闭了,则 read 读完数据会返回0,而不是阻塞;
4、若管道所有读端被关闭了,则write 写数据会触发异常(导致进程退出)SIGPIPE
2、命名管道
命名管道,顾名思义就是一个有名字的管道,体现就是在文件系统中会有一个管道文件;
管道文件给同一主机上的任意进程提供通过打开同一个命名管道进而访问到内核中的同一块管道的缓冲区功能;
命名管道可以从命令行上创建:
$ mkfifo filename
也可以从程序里面进行创建:
#include<unistd.h>
int mkfifo(const char* filename,mode_t mode);
filename:管道文件路径名
mode:管道文件的操作权限
命名管道的特性与匿名管道类似;
3、命名管道和匿名管道的区别
1、匿名管道由pipe函数创建并打开;
2、命名管道由 mkfifo 函数创建,打开用open;
3、FIFO(命名管道) 与 pipe(匿名管道)之间唯一的区别在于它们的创建和打开方式不同,一旦这些工作做完后,它们具有相同的意义;
system V共享内存
共享内存是最快的IPC形式,一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话来说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
共享内存原理:开辟了一块物理内存空间,将这块物理内存空间映射到进程的虚拟地址空间进行操作;若多个进程映射连接到同一块物理内存,则通过这块物理内存实现进程间通信,这种通信相较于其他通信方式,少了用户态和内核态的数据拷贝过程。
共享内存的生命周期:随内核
共享内存的结构体:
//共享内存的数据结构
struct shmid_ds{
struct ipc_perm shm_perm; //operation perms
int shm_segsz; //size of segment(bytes)
__kernel_time_t shm_atime; //last attach time
__kernel_time_t shm_dtime; //last detach time
__kernel_time_t shm_ctime; //last change time
__kernel_time_t shm_cpid; //pid of creator
__kernel_time_t shm_lpid; //pid of last operator
unsigned short shm_nattch; //no. of current attaches
unsigned short shm_unused; //compatibility
void* shm_unused2; //ditto - used by DIPC
void* shm_unused3; //unused
};
共享内存的操作步骤:
创建共享内存 --> 将共享内存映射连接到虚拟地址空间 --> 直接通过虚拟地址进行内存操作 --> 解除映射关系 --> 删除共享内存
#include<sys/ipc.h>
#include<sys/shm.h>
1、创建共享内存
int shmget(key_t key,size_t size,int shmflg);
key:共享内存表示符
由函数 key_t ftok(const char* pathname,int proj_id)创建(通过文件 inode 节点号与一个 pro_id 生成)
size:共享内存大小
shmflg:创建标识
IPC_CREAT (存在打开,不存在创建)
IPC_EXCL (与IPC_CREAT同时使用,若存在则报错,不存在则创建)
mode_flags (权限)
2、将共享内存端连接到进程地址空间
void* shmat(int shmdi,const void* shmaddr,int shmflg);
shmdi:创建共享内存,返回的操作句柄
shmaddr:映射首地址,通常是NULL(会自动选择一个地址)
shmflg:若被指定为 SHM_RDONLY 则只读,否则是可读可写
返回值:成功返回一个指针
3、解除连接
int shmdt(const void* shmaddr);
shmaddr:由shmat所返回的指针
4、删除共享内存(并不是直接删除,要等到映射连接为0的时候)
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
shmid:操作句柄
cmd:将要采取的操作
IPC_RMID:删除共享内存
IPC_STAT: 把 shmid_ds 结构中的数据设置为共享内存的当前关联值
IPC_SET:在进程有足够权限的情况下,把共享内存的当前关联值设为 shmid_ds 数据结构中欧冠给出的值
buf:获取 / 这是共享内存信息(不用获取则置为NULL)
system V消息队列
消息队列其实就是内核中维护的一个队列,提供了一个从一个进程向另一个进程发送一块(有类型)数据的方法;
每个数据块都被认为只有一个类型,接收者进程接收数据的数据块可以由不同的类型值;
特性:每个数据块都由一个特定的类型,接收方可以根据这些类型来有选择地加收数据;
IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核;
system V信号量
内核中的一个计数器 + 等待队列 ---- 用于资源计数 + 等待唤醒功能
信号量的提出是并发编程领域重要的一步,信号的两种状态P(传递) 、V(释放),假设有一个信号量SV,则对他的P、V操作含义:
P(SV):如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行;
V(SV):如果有其他进程因为等待SV而被挂起,则唤醒之;如果没有,则将SV+1;