https://blog.csdn.net/qq_35423154/article/details/105294963
在之前的一篇博客中讲过,因为每个进程都通过自己的页表构建物理地址和虚拟地址的映射关系,使每个进程都拥有自己的虚拟地址空间,并通过这个独立的虚拟地址空间来对物理内存进行操作,所有的进程都只能访问自己的虚拟地址,而不能直接访问物理内存,所以多个进程无法访问同一块区域,无法实现通信。
因为这种独立性,进程之间无法直接进行通信,操作系统就为了解决这种问题,提出了多重适用于不同情境下的通信方式。
ps:这里主要讲的是systemV的,POSIX的后面再写
数据传输:管道、消息队列
数据共享:共享内存
进程控制:信号量
原理:管道的本质其实就是内核中的一块缓冲区,多个进程通过访问同一个缓冲区就可以实现进程间的通信
管道分为两种:匿名管道、命名管道
匿名 管道是内核中的一块缓冲区,因为没有具体的文件描述符,所以匿名管道只能适用于具有亲缘关系的进程间通信。父进程在创建管道的时候操作系统会返回管道的文件描述符,然后生成子进程时子进程会通过拷贝父进程的pcb来获取到这个管道的描述符,所以他们可以通过这个文件描述符来访问同一个管道,来实现进程间的通信。而不具备亲缘关系的进程则无法通过这个文件描述符来访问同一个管道。
#include
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回-1
一开始父进程创建管道
父进程fork创建子进程
关闭多余描述符
就这样,子进程通过写入端fd[1]向管道写入数据,父进程通过读入端fd[0]从管道读取数据,来实现进程间的通信。
#include
#include
#include
int main()
{
int pipefd[2];
pipe(pipefd);
int pid = fork();
if(pid == 0)
{
close(pipefd[0]);
while(1)
{
write(pipefd[1], "hello world", 12);
sleep(3);
}
close(pipefd[1]);
exit(0);
}
else if(pid > 0)
{
close(pipefd[1]);
char buff[1024];
while(1)
{
read(pipefd[0], buff, 12);
printf("%s\n", buff);
}
close(pipefd[0]);
}
return 0;
这是一个简单的管道
因为父子进程究竟是谁先执行这一点我们无法知道,假设如果子进程还没写入,父进程却已经开始读了,这时候应该是会读不到东西的,但是这种情况并没有发生,这里就牵扯到了管道的读写特性。
匿名管道的限制就是只能在亲缘关系的进程间通信,如果我们想为不相关的进程交换数据,就可以使用命名管道。
原理:命名管道也是内核中的一块缓冲区,但是它具有标识符。这个标识符是一个可见于文件系统的管道文件,能够被其他进程找到并打开管道文件来获取管道的操作句柄,多个进程可以通过打开这个管道文件来访问同一块缓冲区来实现通信
接口:
int mkfifo(const char *filename,mode_t mode);
mode:权限掩码
filename:管道的标识符,通过这个标识符来访问管道
返回值:若成功则返回0,否则返回-1
#include
#include
#include
#include
#include
#include
int main()
{
mkfifo("test", 0664);
int pid = fork();
if(pid > 0)
{
int read_fd = open("test", O_RDONLY);
char buff[1024];
read(read_fd, buff, 12);
printf("buff:%s\n", buff);
close(read_fd);
}
else if(pid == 0)
{
int write_fd = open("test", O_WRONLY);
write(write_fd, "hello world", 12);
close(write_fd);
exit(0);
}
return 0;
}
1. 若文件以只读打开,则会阻塞,直到文件被以写的方式打开
2. 若文件以只写打开,则会阻塞,直到文件被以读的方式打开
1. 管道是半双工通信(可以选择方向的单向传输),这个可以从上面的示意图看出来
2. 管道的读写特性(无论命名匿名都一样)
若管道中没有数据则读操作堵塞,如果管道中数据满了写操作堵塞。
如果管道中所有读端关闭则写端触发异常,如果所有写端关闭则读端读完数据后不堵塞返回0
3. 管道声明周期随进程,打开管道的所有进程退出后管道就会被释放。
4. 管道提供字节流传输服务
5. 命名管道额外有一个打开特性,只读打开会阻塞直到被以写打开,只写打开会阻塞直到被以读打开
6. 管道自带同步和互斥
共享内存即在物理内存上开辟一块空间,然后多个进程通过页表将这同一个物理内存映射到自己的虚拟地址空间中,通过自己的虚拟地址空间来访问这块物理内存,达到了数据共享的目的。
如图:
也正是因为这种特性,使得共享内存成为了最快的进程间通信的方式,因为它直接通过虚拟地址来访问物理内存,比前面的管道和后面的消息队列少了内核态和用户态的几次数据拷贝和交互。
1. 创建共享内存
2. 将共享内存映射到虚拟地址空间
3. 进行操作
4. 解除映射关系
5. 释放共享内存
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回共享内存标识符,失败返回-1
头文件:
#include
#include
shmid: 共享内存标识
shmaddr:指定连接的地址
shmflg:权限标志
返回值:成功返回指向共享内存映射在虚拟地址空间的指针(即首地址),失败返回-1
头文件:
#include
#include
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0,失败返回-1
头文件:
#include
#include
shmaddr: 由shmat所返回的指针
返回值:成功返回0,失败返回-1
头文件:
#include
#include
1. 共享内存是最快的进程间通信方式
2. 生命周期随内核
3. 不自带同步与互斥,但可以借助信号量来实现同步与互斥
消息队列是内核中的一个优先级队列,多个进程通过访问同一个队列,进行添加节点或者获取节点来实现通信。
key:消息队列对象的关键字
msgflg:消息队列的建立标志和存取权限
返回值:成功执行时,返回消息队列标识值。失败返回-1
头文件:
#include
#include
#include
msqid:消息队列对象的标识符
msgp:消息缓冲区指针
msgsz:消息数据的长度
msgtyp:决定从队列中返回哪条消息
msgflg:消息队列状态
返回值:成功执行时,返回0。失败返回-1
头文件:
#include
#include
#include
msqid:消息队列对象的标识符
cmd:函数要对消息队列进行的操作
buf:取出系统保存的消息队列的msqid_ds 数据,并将其存入参数buf 指向的msqid_ds 结构中
返回值:成功执行时,返回0。失败返回-1
头文件:
#include
#include
#include
1. 自带同步与互斥
2. 生命周期随内核
信号量其实是内核中的一个计数器和阻塞队列,通过信号量来对临界资源的访问进行控制,来实现进程间的同步与互斥。
例如有一个能容纳n人的餐厅,则用一个计数器表示n,如果有人进入则n - 1,如果有人出来则n + 1,只有n > 0时才能进入,如果n <= 0时,则说明没有位置,需要将进程挂起并放入阻塞队列中,直到有人出来使资源释放时,才能将后续进程从阻塞队列中唤醒获取资源
同步:
通过条件判断实现临界资源访问的合理性
互斥:
通过同一时间的唯一访问来实现临界资源访问的安全性