type
status
summary
date
slug
tags
category
icon
password
进程间通信
1.进程间通信基本介绍
🤨
要介绍就先讲讲为什么要进程间通信:
1.数据传输,将他的数据传输到另外一个进程
2.资源共享,多个进程之间共享同样的资源
3.通知时间:一个进程需要向另外一个或一组进程发送消息,通知他们发生了某种时间(如进程终止需要通知父进程)
4.进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望进程能够拦截另一个进程的所有陷入和异常,并能够知道他的状态改变
😄
进程间通信的发展
1.毫无疑问——管道
2.System V进程间通信
3.POSIX进程间通信
🙂
进程间通信分类
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V消息队列
- System V共享内存
- System V信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
ok,简单介绍到此结束。。。
1.管道
其实管道时Unix系统中最古老的进程间通信的形式,我们把从一个进程连接到另外一个进程的一个数据流称为一个管道。
注意管道只能从一个进程单向的将数据传入,并且只能用于有亲缘关系的进程之间进行通信
其实进程间通信的本质就是让两个进程看见同一块操作系统提供的资源,注意必须是操作系统提供的,因为如果是进程本身体统的,因为进程具有强独立性,且破坏独立性是操作系统不允许的,所以进程本身的资源是不允许传递给其他进程的
我们进程访问空间,进行进程间通信,本质就是访问操作系统,而进程代表的是用户,用户不能直接访问操作系统内核数据,所以操作系统提供了系统调用接口,所以是从操作系统底层设计,从接口设计,一个独立的通信模块IPC —— 隶属文件系统。当进程通信变多,显而易见,操作系统需要将他们管理起来,先描述再组织。
所以本质上是操作系统提供了资源让不同的进程看见,这样进行了通信,当操作系统中不同的模块提供资源是就出现了多种进程通信方式
当一个进程创建了管道文件后他的files_struct中会新增两个fd一个用于写入管道文件(通常被称为写端)一个用于读取管道文件(通常被称为读端),这样进程就能在这个文件中进行读写,但是由于文件是在task_struct中的file_struct中的所以只能是有亲缘关系之间的进程才能获得这个文件的fd,当所有指管道文件的文件描述符都被关闭之后管道中的数据就被丢弃,管道本身也被销毁
由此看来管道是一个简单但强大的通信功能,他适用于需要数据流的场景,如父子间的数据传递,但是因为他的单向性和容量有限性,管道不适用于所有的通信场景,在这种i情况下需要考虑其他的ipc机制如消息队列,共享内存,或者套接字等
例如,统计当前使用云服务器上的登录用户个数
who命令和wc都是两个程序,也都是bash的子进程,也就是兄弟进程who进程将数据流传入到管道文件中wc再读取,进而进一步的完成数据的处理
管道文件其实是一种内存级的文件不会再磁盘中存在
,而进程写入到管道文件的数据流存放在文件和的文件页缓冲区中,如果是对于磁盘文件来说,无论读方式还是写方式都会刷新缓冲区数据到磁盘中,但是操作系统肯定不会允许这种事情发生(因为会降低IO效率)所以管道文件只是采取了文件的方式,但是他其实是一种内存级的文件
由于匿名管道不直接对应于文件系统中的任何实体,因此他没有自己的inode号码。完全是靠着父进程拷贝继承而来的
以上其实是匿名管道,接下来讲讲什么是命名管道,
命名管道顾名思义其实就是有名字,inode号码,和明确的路径,可以通过文件系统的接口进行访问和操作
对于父进程来说父进程必须读写端全部打开,因为如果只打开一个的话子进程靠复制过来的files_struct中的管道文件页只能读或者写这样就矛盾了
2.pipe系统调用
该函数参数是一种输出型参数,什么意思呢》其实就和waitpid中的status参数一样,就是执行这个函数之后,pipefd能带回来两个文件描述符号
pipefd[0]:读下标
pipefd[1]:写下标
调用成功返回0失败返回-1
3.管道内自带同步和互斥机制
当多执行流共享时候,可能会出现冲突问题,即一个进程再访问数据的时候,另外一个进程正在写入数据,这个就可能覆盖元数据——临界资源竞争问题
临界资源是需要被保护的,如果我们不管对管道这种临界资源进行任何的保护机制,就可能出现同一时候有多个进程对同一个管道进行操作的情况,进而导致同时读写,交叉读写以及读取到的数据不一致等问题
为了避免这些问题,内核会对管道操作进行同步和互斥操作:
- 同步:两个或者两个以上的进程再运行过程中协同步调,按照预定的先后次序运行,比如A任务的运行依赖B任务产生的数据
- 互斥:一个公共资源同一时刻只能被一个进程使用,多个进程不能同时使用公共资源
实际上,同步时一种更为复杂和的互斥,而互斥是一种特殊的互斥,对于管道场景来说,互斥就是两个进程不能同时对管道进行操作,他们会互相排斥,必须等一个进程操作完毕另一个进程才能进行操作,而同步也是指这两个不能同时对管道进行操作,但是这两个进必须要按照某种次序来对管道进行操作
4.管道通信的特征
1.亲缘性:具有血缘关系的进程才能通过管道通信
2.单向性:管道是单向的,数据只能从一个方向流动
3.通信进程之间会协同(互斥、同步),这是为了保护管道文件的数据安全。如果父进程读数据时,缓冲区直接拿来就读会导致乱码,因为缓冲区不是每时每刻都为空,子进程也不是时时刻刻写入缓冲区,所以进程间需要协同!一方没有写入,另一方就不读写
4.基于字节流:管道是面向字节流的,管道中的数据以字节流的形式传递,没有消息边界的概念(不管一个进程写了多少字符、写了几次,另一个进程读取时一次全部读完。就像是自来水,来自写端的自来水不分边界,不管写端放了多少自来水,读端都是一个读完,不管读端是拿盆还是拿桶来接)对于进程A写入管道当中的数据,进程B每次从管道读取的数据的多少是任意的,这种被称为流式服务
5.生命周期:匿名管道的生命周期随进程结束而结束;命名管道的生命周期则取决于文件系统,除非显式删除,否则会一直存在。管道是基于文件的,文件的生命周期是跟随进程的。父子进程退出时,管道文件自动被操作系统释放,例如默认打开的stdin、stdout、stderr都是操作系统关闭的
如果想要用管道来实现双向通信,其实创建两个管道就可以了
pipe是有大小的,可以使用ulimit -a能查看,会发现大小是4kb的,但是其实大小是64kb的
管道的四种情况:
1.读写端正常,如果管道为空,读端就要阻塞
2.读写端正常,如果管道被写满,写端被阻塞
3.读端正常,写端关闭,读端就会读到零,表明读到了pipe的结尾,不会被阻塞
- 写段正常,读端关闭,操作系统就会杀掉正在写入的进程
杀掉之后如果这个时候读端开启,就会变成3号问题然后退出
2.命名管道
对于匿名管道来说,只能对应于有亲缘关系的进程来进行通信,
那如果没有亲缘关系的进程想要进行通讯呢?这个时候就要用到命名管道了
其实命名管道也是内存级文件,但是他有名字并且有inode号码是存在于磁盘系统的文件但是操作系统是不会让管道文件的内容刷到磁盘上的,所以命名管道也是一个内存级文件
通过文件名让两个没有亲缘关系的进程看见同一份资源
日志(log)
日志时间,日志的等级,日志内容,稳健的名称和行号
info:常规消息
warning:报警消息
error:必要严重,需要立即处理
fatal:致命错误
我们来实现一个简单的日志函数
client.cc
comm.hpp
log.hpp
server.cc
4.共享内存
systemV 共享内存
还是那句话:进程间通信的本质就是先让不同的进程看到相同的资源
系统调用函数
其中我们发现返回值是int类型的是共享内存的表示符号
中间的那个size_t size参数是创建的共享内存的大小(单位是字节)
第三个参数就和open中的第二个参数很像了
有三个两个宏一般有三种使用方式:
IPC_CREAT(单独使用):如果申请的共享内存不存在就创建,存在就获取并且返回
IPC_CTREAT|IPC_EXCL:如果申请的内存不存在就创建,如果存在就报错返回,用来确保我们获得的这块内存一定是一块新的内存
IPC_EXCL:不推荐单独使用
第一个参数key:
key再系统内核中具有唯一性,能够让不同的进程进行唯一性标识第一个进程可以通过key创建共享内存,之后的第二个进程,只要拿着同一个key就能和第一个进程看到同一个共享内存,对于一个已经创建好的共享内存,key再共享内存的描述对象中(类似于文件的file-struct)
通过这个函数得到一个key这个函数本质上是一套算法:通过路径和proj_id的得到的一个key其实就是有用户自由指定的一key罢了
在系统中查看共享内存的指令是ipcs -m
共享内存的生命周期是随内存的,用户如果不主动关闭,共享内存会一直存在除非用户释放或者内核重启
释放共享内存的指令(在用户层统一使用shmid)
当然共享内存也要设置权限,直接在shmget函数的第三个参数处|0666(mode)
其中共享内存的大小一般推荐是4096bit的整数倍
如果设置的是4097,那其实操作系统分配的是4096*2
共享内存数据结构:
共享内存函数:
1.shmget函数:
用于创建共享内存
函数原型:
int shmget(key_t key,size_t size,int shmflag);
参数
key:这个共享内存段名字
size:大小
shmfalg:由九个权限构成,他们的用法和创建文件时使用的mode模式标志时一样的
返回值:成功返回一个非负数shmid,失败返回-1
2.shmat
功能:将共享内存段链接到进程地址空间
原型:
void *shmat(int shmid,const void *shmaddr,int shmflag)
参数:
shmid:函数shmid的返回值
shmaddr:指定链接的地址
shmflag:他们两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个地址,失败返回-1
shmaddr为nullptr,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_SND标记,则链接的地址会自动向下调整为shmlaba的整数倍
shmfalg为SHM_RDONLY表示只读共享内存
shmdt函数
功能:将共享内存段与当前进程脱离
原型:
int shmdt(const void *shmaddr)
参数:
就是函数shmat函数所返回的指针
注意脱离进程不代表删除共享内存
shmctl函数:
功能用于控制共享内存
原型:
int shctl(int shmid,int cmd,struct shmid_ds *buf)
参数第一个不介绍
第二个:将要采取的动作(三个可取值)
buf:指向一个保存这共享内存的模式状态和访问权限的数据结构
cmd
1.IPC_STAT:把shmid_ds结构中的数据设置为共享内存当前的关联值
2.IPC_STE:在进程有足够权限的前提下,把共享内存当前的关联值设置为shmid_ds数据结构中给出的值
3.IPC_RMID:删除共享内存段
- Author:PytC
- URL:https://PytC.fun//article/14749b4c-fd33-8057-a0f0-f97a91e8e3bf
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!