1.进程间通信
如果一个系统是单进程的,那就无法使用并发能力。进程间通信是实现多进程协同的手段,目的是完成进程间的数据传输、消息通知、同步执行流等。
进程具有独立性! 虚拟空间 + 页表的方式保证了进程的独立性。进程间通信的本质,前提是要进程看到同一块"内存",这个"内存"不隶属那个进程,是进程间共享的!
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
2.管道
$ ps axj | grep mysql
Linux指令中「|
」竖线就是一个管道,它的功能是将前一个命令(ps auxf
)的输出,作为后一个命令(grep mysql
)的输入。可以看出管道传输数据是单向的,即半双工。
从一个进程连接到另一个进程的一个数据流称为一个"管道",它是Unix中最古老的进程间通信的形式。管道分为匿名管道 和 命名管道。
2.1.匿名管道
站在文件描述符角度-深度理解管道
站在内核角度-管道本质
子进程 fork 来复制父进程 fd 文件描述符,让父子进程通过文件看到同一块空间。将资源(数据)放到内存中(缓冲区中)没有必要将数据写到磁盘中,这样能提高效率。
管道读写规则
- 写端快读端慢:管道写满不能再写
- 写端慢读端快:管道没有数据时,读端必须等待
- 写端关闭读端read返回0,标识读到文件结尾
- 读端关闭写端继续写,操作系统会终止写端进程
管道特点
- 管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常用于父子进程通信
- 管道通过控制读写端的速度,提供了访问控制
- 管道提供的是面向流式的通信服务,即管道是面向字节流的
- 管道是基于文件的,而文件的生命周期随进程,所以管道的生命周期随进程的
- 管道是单向通信的,是半双工的特殊情况
2.2.命名管道
对于匿名管道,它的通信是具有血缘关系进程间通信。通常通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。而对于命名管道,它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。
不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则。
在命令行中使用:
使用命令mkfifo
命名管道:
xiyan@VM-20-5-ubuntu:~/code/namePipe$ mkfifo fifo.ipc
代码中使用:
int mkfifo(const char *filename,mode_t mode);
定义一个路径,服务端创建命名管道,并以读的方式打开对应的路径文件,而客户端以写的方式和服务器打开相同路径下的文件(即:命名管道)
3.共享内存
操作系统在物理地址创建一块共享内存,进程通过页表建立映射,将共享内存的地址映射到自己虚拟地址空间的共享区中,然后通过地址加偏移量的方式来访问同一块内存。
如果多对进程创建了多个共享内存,操作系统需要管理共享内存,如果重新理解共享内存 = 共享内存块 + 对于共享内存的内核数据结构
共享内存的方式,进程能从同一块空间访问,没有拷贝,是进程通信中速度最快!但是缺乏访问控制,但是可以通过加上管道的方式进行控制。
3.1.使用系统调用创建共享内存
基本步骤:
系统调用
创建公共key值
key_t ftok(const char *pathname, int proj_id);
shmServer端和shmClient端传入的参数要是一致的!
ftok会根据pathname的inode和proj_id生成一个唯一的数,这和hash的原理或者是MD5的加密算法原理是相同的。
创建共享内存
int shmget(key_t key, size_t size, int shmflg);
参数说明:
key :内核中共享内存的标识符(内核层),多个进程通过相同的标识符才能打开同一个共享内存,我们通过ftok函数获取。
size:共享内存的大小,以内存页(4096byte)为单位的整数倍进行分配;
shmflg:对于shmServer端传入的参数是**(IPC_CREAT | IPC_EXCL | 0666),IPC_CREAT单独表示如果共享内存存在,获取返回,如果不存在创建返回;IPC_CREAT | IPC_EXCL的整体表示如果共享内存存在,出错返回,如果不存在创建返回。对于0666表示设置的文件权限。对于shmClient端传入的参数(key,SHM_SIZE,0)**
返回值:
成功返回一个非负整数,即该共享内存段的标识符(用户层),类似文件标识符fd;失败返回-1
建立连接
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
shmid:共享内存标识 ,即:shmget函数的正确的返回值
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY 。
返回值:
返回值:返回共享内存映射在虚拟地址空间的首地址,可以使用首地址进行内存的操作,这就类似malloc函数的操作。 失败返回-1
断开连接
int shmdt(const void *shmaddr);
参数说明:
shmaddr:映射在虚拟地址空间的首地址,即shmat的正确返回值。
删除共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
参数 shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作,传入 IPC_RMID表示要删除共享内存段
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
3.2.Linux操作进程通信资源命令
xiyan@VM-20-5-ubuntu:~/code/shm$ ./server
[Info][2024-3-19 10:18:43] create ftok success : key is 0x:66020028
Segmentation fault (core dumped)
查看共享内存:
`ipcs` 查看进程间通信资源
`-m` 查看共享内存
`-q` 查看消息队列
`-s` 查看信号量
xiyan@VM-20-5-ubuntu:~/code/shm$ ipcs -m
删除
ipcrm -m/q/s shmid
xiyan@VM-20-5-ubuntu:~/code/shm$ ipcrm -m shmid
总结:用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。这时候要可以用到信号量的PV操作,保证原子性。多进程版的PV操作用得少 。
4.消息队列
管道的通信方式是效率低的,因此管道不适合进程间频繁地交换数据。
通信的进程将数据放到消息队列。如:A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。而所谓的消息队列是保存在内核中的消息链表。消息队列的生命周期随内核,需要释放。
消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAX
和 MSGMNB
,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
5.信号
正常的情况下,我们可以是使用管道和共享内存等的方式来进行进程间的通信。**对于异常情况下的工作模式,需要用「信号」的方式来通知进程。**比如,我们要结束一个前台进程可以使用热键[Ctrl + c]的方式来产生信号来结束进程。
后续分享~
6.Soket
管道、消息队列、共享内存、信号都是在同一台主机上进行进程间通信,那要想**跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。**通过域间套接字,Socket也可以在同一台主机完成通信。
后续分享~