一. 进程间通信
虽然进程间具有独立性,但其也可进行进程通信
1. 进程间通信的目的
- 数据传输:一个进程需要将它的数据传递给另一个线程
如:who | grep anthony,需要把who这个进程运行的结果传递给后面 - 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送信息,通知它(它们)发生了某种事件(如进程终止要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如:Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它们的状态改变(如父进程控制子进程)
2. 进程间通信发展
进程间通信简称 IPC
- 管道
- System V进程间通信
- POSIX进程间通信
3. 进程间通信分类
- 管道:
匿名管道(pipe),命名管道 - System V IPC
消息队列、共享内存、信号量 - POSIX IPC
消息队列、共享内存、信号量、互斥量、条件变量、读写锁
4. 怎样实行通信,以及为什么会有不同的通信方式
让两个进程之间实行通信:让两个进程看到同一份资源
这个资源由谁提供,以什么方式提供的,就决定了不同的通信方式
二. 管道
这个资源(内存)由OS以文件形式提供就为管道
(在文件 inode 信息中可以判断其是否为管道)
1. 什么是管道
管道是 Unix 中最古老的进程间通信的形式
我们把从一个进程连接到另一个进程的一个数据流称为一个”管道“
一个进程拥有以下结构
task_struct、mm_struct、页表、file_struct、fd_array、inode
假设左为父进程,右为子进程
父进程往 page 中写入,子进程可以读取父进程写入的内容
所以这就是管道
2. 匿名管道
用于有亲缘关系的进程(如父子进程)
如 xxx | xxx …左写右读
可把管道当作文件看待(Linux中一切皆文件)
who | wc -l
把 who 本应写到显示器的内容写到文件(管道),再从管道输出到 wc -l 进程中
如何创建
#include <unistd.h>
int pipe(int fd[2]);
参数:
1. fd是文件描述符数组,其中fd[0]是读端,fd[1]是写端,相当于将一个文件以读写两种形式打开。相当于返回建立管道文件的文件描述符。
返回值:
成功返回0,失败返回错误码
- 创建管道时,会先打开文件,不能用 open 方式打开,而用 pipe去打开(读写形式打开)
(将一个文件分别以读、写两种形式打开,打开的是一个文件,以两种形式打开) - 在 fd 中,fd[0]是读,fd[1]是写
能直接在进程中用一个 char buffer[1024] 作缓存,父进程写,子进程读吗?
这样是不行的,父子进程数据是独立的,父进程去写的时候,会发生写时拷贝,父子进程访问的即不是一个数据
具体实现
int main(){
int pipefd[2] = {0};
pipe(pipefd);
pid_t id = fork();
if(id == 0){
close(fd[0]);
const char* msg = "I am child\n";
while(1){
write(pipefd[1],msg,strlen(msg));
sleep(3);
}
}
else{
close(pipefd[1]);
char buffer[1024];
while(1){
size_t s = read(pipefd[0],buffer,sizeof(buffer);
if(s > 0){
buffer[s] = 0;
printf("father get message:%s\n",buffer);
}
}
}
return 0;
}
- 如果子进程不将数据写入,那么父进程将不会读取且打印。也就是说子进程如果 sleep(3) ;那么父进程也 sleep(3) ;父进程发生了 阻塞
- 整个读写节奏,是按 写 的节奏进行
- 如果写端不关闭文件描述符,读端可能会阻塞
- 如果读写端都不 sleep 一下,或者让读端 sleep一下,写端会一下将管道写满进入 阻塞 ,等待读端读出后,写端再进行写入
(读取和写入不是写一条,读一条,有可能是一次写入很多,也有可能是一次读取多个数据,也受传入读取大小的影响)
所以在管道中,读取或写入条件不满足时,读取端或写入端都要被阻塞 - 如果写端关闭文件描述符,读端在读完管道数据后,会读到文件结尾
- 如果读端关闭,写入端的数据没有人去读取,写端可能会被 OS 直接杀掉(OS 不做浪费系统资源,降低效率的事)(用信号将其杀死)
3. 命名管道
可在不相关的进程之间交换数据,可使用 FIFO 文件来做,它叫做命名管道,命名管道是一种特殊类型的文件。
创建管道
- 命名管道可以从命令行上创建 (可在两个终端进行通信)
mkfifo filename
- 命名管道也可从程序创建
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char* filename,mode_t mode);
参数:
* 你想创建的管道对应的路径+名称
* 创建的管道的权限
返回值:
返回-1,则调用失败
命名管道的特点
- fifo 是一个特殊文件,大小始终为0,它是 OS 为了两个进程进行通信,在内存中创建的管道文件,所以两个进程通信的数据就不会刷新到 磁盘上。
- 通信时怎么退出管道?因为进程退出,则管道退出,所以ctrl + c关闭即可
4. 匿名管道与命名管道的区别
- 匿名管道由 pipe 函数创建且打开
- 命名管道由 mkfifo 函数创建,打开用 open
- 匿名只是 亲缘关系,命名是 不同进程
- 命名管道与匿名管道之间唯一区别在于它们打开方式不同,一旦完成这些工作,它们有相同语义。
- 命名管道文件只是缓存区的标识,删除命名管道文件后,进程还能通信,因为管道声明周期随进程,管道文件只是标识
- 管道只能单向通信
三. 共享内存
跳过文件,直接在系统内核层帮我们构建通信
1. 共享内存
共享内存是 最快的IPC 形式,一旦这样的内存映射到共享它的进程地址空间,这些进程间数据的传递不再涉及到内核。进程不再通过执行进入内核的系统调用来传递彼此的数据。
2. 共享内存原理
管道存在用户到内核,内核到用户的拷贝,但此时共享内存不存在拷贝,一个进程往里面写,另一个进程拿就即可,所以只存在外设与用户的拷贝。
3. 共享内存的数据结构
共享内存也需管理,并且还有多块共享内存。
所以也需要先描述再组织,构建数据结构进行描述与组织。
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_ipc_pid_t shm_cpid /* pid of creator */ (共享内存谁创建的)
__kernel_ipc_pid_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 */
};
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
靠key区分不同共享内存,标识共享内存的唯一性。
mode代表ipc资源权限
形成共享内存的key
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char* pathname,int id);
参数:
1. filename就是你指定的文件名(该文件必须是存在而且可以访问的)
2. id是子序号,虽然为int,但是只有8个比特被使用(0-255)。
返回值:
struct ipc_perm中的key
4. 共享内存函数
shmget
创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key,size_t size,int shmflg);
参数:
1. 这个key被填到ipc_perm中作为共享内存的唯一标识(在OS上标识)
2. 共享内存的大小(最好以页为单位,4096字节)
3. 有几个权限标志位
返回值:
成功返回这个共享内存的标识符(给用户使用的唯一标识,要与ipc_perm中的key区分开),失败返回-1
shmflg的几个标志
1. IPC_PRIVATE
2. IPC_CREAT (如果该共享内存有了,就打开返回)
3. IPC_EXCL (如果该共享内存有了,就出错返回)
如果我们非要开4097的字节空间,但实际是开了两页的空间,但你越界照样会报错
IPC资源生命周期不随进程,随内核(如果我们不清除,将会一直占用,除非重启)
ipcs -m查看共享内存
ipcrm -m 标识符x(shmid,在用户层标识的)
删除标识符为x的共享内存
shmctl
用于控制共享内存(可删除共享内存)
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
参数:
1. 共享内存的标识符
2. 将要采取的动作(我们控制它,只用删除:IPC_RMID)
3. 指向一个保存着共享内存模式状态和访问权限的数据结构(一般不用,设为NULL)
shmat
将共享内存段连接到虚拟地址空间(挂接)
#include <sys/types.h>
#include <sys/shm.h>
void* shmat(int shmid,const void* shmaddr,int shmflg);
参数:
1. 共享内存标识符
2. 挂接时,要不要指定虚拟地址,设为空即可(标识OS自己去选)
3. 默认写为0即可
返回值:
成功返回一个指针,返回该关联虚拟地址空间的其实地址
失败返回-1
shmdt
将共享内存段与当前进程脱离(取消挂接)
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数:
与shmat第二个参数对应
返回值:
成功返回0
失败返回-1
要让两个进程挂接上一个共享内存
需要都调用 ftok,找到相同的key,再与其挂接
5.总
我们建立共享内存,让两个进程挂接时,先让一个进程创建一个共享内存,再将自己进程与共享内存关联;另一个进程也找到该共享内存与其关联,不必创建。
删除时,建立共享内存的进程,取消挂接,删除共享内存。另一个进程也为取消挂接
共享内存底层不提供任何同步与互斥机制,它们各自就像普通程序一样运行(不会堵塞,它们两个完全不知道对方存在)
在底层是通过key找到同一个共享内存,在用户通过标识符(shmid),这些函数找共享内存用标识符
五. 消息队列
消息队列提供一个从一个进程向另外一个进程发送一块数据的方法
每一个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
1. 创建消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msggt(key_t key,int msgflg);
参数:
1. 消息队列的唯一标识
2. 与共享内存一致
返回值:
成功返回消息队列的标识符,出错返回-1
其他函数基本与共享内存一致
消息队列的数据结构中也有 ipc_perm
六. 信号量
信号量主要用于同步与互斥的
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
1. 进程互斥
- 由于各进程要求共享资源,而且有些资源需互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥
- 系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源
- 在进程中涉及到互斥资源的程序段叫临界区
- 特性方面:IPC资源必须删除,否则不会自动清除,除非重启,所以 System V IPC 资源的生命周期随内核
总
- 套接字也是进程间通信
- 管道的本质是内核的一块缓冲区
- 共享内存只有在当前映射连接数为0,才会被真正删除
- ipcs查看进程间通信资源/ipcrm删除进程间通信资源
-m针对共享内存
-q针对消息队列
-a针对所有资源