每个进程都拥有自己独立的虚拟地址空间和页表结构,促使了进程独立,导致了进程和进程之间相互协作工作的问题,为了解决这种问题,才产生了进程间通信。
进程间通信种类:
数据传输
数据共享
进程控制
管道:
管道就是内核当中的一块内存,相当于内核为进程间通信而创建的缓冲区
匿名管道
匿名管道的特性:
- 匿名管道用于具有亲缘关系的进程
- 管道是一个半双工,数据流向只能是一个流向.
- 提供字节流服务
读端没有及时进行读的话,但写端往管道里面写了,后面写的数据会追加在之前写的数据后面.数据其实都是二进制 存储的 .没有明确的数据边界,如果两次的数据表达的是不同内容,有可能导致读端获取数据之后,无法知道写端表达的意思.
读端读数据的时候,是从管道当中将数据拿走了.(被读走的数据不会存留在管道的内存当中)
- 生命周期随进程
PIPE_SIZE 64K,匿名管道的大小 65536
PIPE_BUF 4K, 保证写入或读取数据的原子性 4096
当操作管道的数据小于PIPE_BUF的时候,管道保证进程在读取和写入数据时候的原子性
原子性:当前的操作不能被打断,运行的结果只能有两个,要么是操作完成(1),要么是操作没有完成(0)
文件描述符是阻塞的情况下
1.管道当中没有数据,调用read会进行阻塞,等待写端进行写
2.管道被写满的时候,调用write会进行阻塞,等待读端进行读
匿名管道的创建:
int pipe(int fd[2])
fd[2]出参。我们在使用该函数的时候,需要传入一个int类型大小为2的数组。在函数内部会将数组的值进行补充,调用完成之后,我们就能拿到管道对应的读写端。
fd[0]:读端,用户可以通过fd[0]当中保存的文件描述符来对管道的内存进行读操作
fd[1]:写端
成功返回0,失败返回-1
当创建出来的匿名管道,更改对应读写端文件描述符为非阻塞状态
1.不进行读,一直写:设置写端为非阻塞
- 读端不关闭,调用写:write会返回-1,报错当前资源不可用
- 当所有的读端全部关闭,调用写,会造成写端的进程收到SIGPIPE信号,写端程序会被杀死,管道破裂
2.不写,进行读:设置读端为非阻塞,写端不用关心
- 写端不关闭,读端进行读,read调用返回-1,返回资源不可用
- 当所有的写端全部关闭,调用读:read是正常调用的,read返回的是读到的字节数量。
设置文件描述符属性的函数
int fcntl(int fd, int cmd ,...../*arg */)
fd:文件描述符。
cmd:命令,想让fcntl函数执行的操作
- F_GETFL:获取当前文件描述符属性
- F_SETFL:设置当前文件描述符属性
返回值:文件描述符的属性会通过该函数的返回值返回回来,(使用的时候是位图的方式)
非阻塞属性 O_NONBLOCK
如果返回值是flags
flags|=O_NONBLOCK
设置为非阻塞状态
临界资源:同一时间,当前的资源只能被一个进程所访问
由于不同的进程对资源的访问(读,写,修改),可能会造成数据的二义性
如何保证对临界资源访问的时候,不会造成数据的二义性。
互斥:同一时间,保证只能有一个进程访问临界资源
同步:保证对临界资源访问的合理性
命名管道:
命名管道是具有标识符的管道,内核当中开辟的内存是有标识的,不同的进程可以通过名字访问到命名管道.生命周期随进程。
管道文件:P
如何创建命名管道?
使用命令创建
$mkfifo [命名管道的文件名称]
使用函数创建
int mkfifo (char* pathname,mode_t mode)
mode:指定创建出来的命名管道的读写权限。
system V共享内存
创建共享内存&通信流程
- 先在物理内存当中开辟一段空间
- 各个进程通过页表结构将物理内存映射到自己的虚拟地址空间当中的共享区
- 各个进程之间的通信是通过修改自己虚拟地址空间当中的共享区的地址来完成的。
特性:
不同的进程对共享内存区域进行读的时候,并不会抹除物理内存当中的值。
创建共享内存&使用共享内存的接口
创建共享内存
int shmget(key_t key,size_t size,int shmflag)
key :共享内存的标识符
size:共享内存的大小。
shmflag:
- IPC_CREAT:如果想获取的共享内存不存在,则创建共享内存。如果存在则返回共享内存操作句柄
- IPC_EXCL | IPC_CREAT :如果想获取的共享内存 存在,则报错。
- 权限:可以使用8进制的数字来进行参数的按位或
返回值:成功返回共享内存的操作句柄
失败返回-1
将进程附加到共享内存上
void* shmat(int shmid ,const void* shmaddr, int shmflag)
shmid:共享内存的操作句柄
shmaddr:程序员去指定映射到共享区的哪一个地址。一般情况下,设置为NULL,由操作系统来指定将内存映射到哪一个地址上。
shmflag:
- 0 :可读可写
- IPC_RDONLY:只读
返回值:成功:返回映射到共享区的那一个地址上,用户可以操作这个地址来对物理内存区域进行读写操作。(即返回一个指针,指向共享内存的开始)
失败:-1
从共享内存当中分离进程
int shmdt(const void* shmaddr)
shmaddr:共享区当中映射的无内存的首地址,shmat的返回值。
返回值: 成功:0
失败:-1
共享内存的销毁
int shmct(int shmid, int cmd ,struct shmid_ds* buf)
shmid:共享内存的操作句柄
cmd:
- IPC_RMID:删除共享内存,标记共享内存为删除状态
- IPC_STAT 获取共享内存状态,需要搭配struct shmid_ds
buf:出参,用来获取共享内存的状态信息,传入一个struct shmid_ds结构体对象的地址。
返回值: 成功:0
失败:-1
共享内存的生命周期是跟随操作系统内核的
ipcs 或ipcs -m查看共享内存
eg: ipcrm -m [shmid]
如果删除一个有进程附加的共享内存,操作系统的做法是:
- 先标记当前的共享内存为destory状态,并且将key设置为0x00000000,表示当前的共享内存不能被其他进程所附加,同时会释放共享内存
风险:导致正在附加在该共享内存上的进程有崩溃的风险,一般禁止删除有进程附加的共享内存
- 当进程退出的时候,操作系统就会将描述共享内存的结构体也释放掉。
消息队列
队列的特性:
- 先进先出,底层的实现是链表,在内核当中创建。
- 在队列当中每一个元素都有自己的类型,类型之间有一个优先级的概念
- 每一个节点发送的字节数量 8192即8k
- 队列当中所有消息的长度之和为16384
- 系统当中最大的队列数2379
消息队列的特征:
- 消息队列的生命周期跟随内核
- 消息队列可以进行双工通信
- 克服了管道的无格式的字节流的缺点
获取系统V消息队列标识符
int msgget(key_t key,int msgflg)
key:消息队列的标识符
msgflg:
- IPC_CRETA
- IPC_CRETA | EXCL
- 按位或上权限
System V消息队列操作
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid:消息队列的操作句柄
msgp:发送的数据
msgsz:数据的长度
msgflg:
- 0:当队列满了,则阻塞
- IPC_NOWAIT:如果队列满了,则当前发送的操作不会进行阻塞,函数返回
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msgtyp:
- 0:表示什么类型都可以接受
- >0 返回队列当中消息类型为msgtype的第一个消息
- <0 返回队列当中消息类型小于等于msgtype绝对值的消息
- 如果这样的消息比较多,则返回类型最小的那个消息
System V消息控制操作
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
cmd:
- IPC_STAT
- IPC_SET
- IPC_RMID
信息量 system V
作用:实现进程控制,换句话说,实现了同步与互斥
信息量的本质:
信号量的本质是一个计数器+PCB等待队列
计数器的含义是对资源的计数,对资源进行+1或者-1操作。
互斥
如何实现一个互斥
前提条件:信号量当中的资源计数器只有两个取值,也就是只有0或者1, 0表示当前资源不可用,1表示当前资源可用。
访问:当一个进程需要访问一个临界资源的时候,先获取信号量,预算信号量当中计数器的值
预算:对当前信号量当中的计数器进行-1操作,判断当前信号量当中的计数器是否小于0
- 如果信号量当中的计数器值小于0,则表示信号量之前的值为0,则表示当前的临界资源不可以被访问,将当前获取信号量的进程放到PCB等待队列当中去,进行阻塞等待。
- 如果信号量当中的计数器值等于0,则表示信号量之前的值为1,则表示当前的临界资源是可以被访问的。对信号量当中的计数器进行减1操作,访问临界资源。
释放:
如果临界资源访问完成,需要结束对临界资源的访问,需要对信号量当中的计数器进行+1操作
唤醒PCB等待队列当中的进程
减1操作称为P操作
加1操作称为V操作
同步:
保证进程对临界资源访问的合理性
信号量=计数器+PCB等待队列
计数器的取值不在限制为只能取值为0或者1了,取值可以是任何整数。
1.初始化信号量
将信号量当中的资源计数器设置为资源的数量
2.访问临界资源
访问信号量---------->>对资源计数器进行-1操作,判断资源计数器的值是否大于等于0
如果小于0,则将该进程放到PCB等待队列当中去
如果大于等于0,则访问临界资源
3。释放资源,通知PCB等待队列的做法
当临界资源被进程所释放,就会对信号量当中的资源计数器进行加1操作,通知PCB等待队列当中的进程访问临界资源
- 计数器加1操作完毕之后,计数器当中的值小于等于0的,需要通知PCB等待队列当中的进程
- 计数器加1操作完毕之后,计数器当中的值大于0的,就不需要通知PCB等待队列(PCB等待队列当中也没有进程在等待)