目录
进程间通信的介绍
进程间通信:进程之间的沟通交流
因为进程的独立性,所以导致进程间的数据通信将变得非常麻烦。
在实际工作中往往会出现一个系统中好几个进程协同工作,那么这些进程就需要交流沟通,完成协作。
操作系统不得不提供方法来使进程间能够通信。
因此就产生了进程间通信方式,来解决如何进行进程间通信的问题。
为什么要进程通信:
进程之间的协作(事件通知,数据传输,进程控制)
因为通信的公共介质有所不同,所以通信的方式不止一种。
进程间通信方式:
- 管道(匿名管道/命名管道)
- 共享内存(System v IPC)
- 消息队列(System v IPC)
- 信号量(System v IPC)
管道
我们把从一个进程连接到另一个进程的一个数据流成为一个“管道”
传输数据资源
原理:操作系统为进程提供一个双方都能访问内核的一块缓冲区,操作系统提供的管道操作接口就是基础io接口
半双工:单向通信(所以必须确定方向)
匿名管道
匿名管道:
不可见 于文件系统,创建的缓冲区是没有名字的仅仅适用于具有亲缘关系的进程间通信,因为匿名管道其他进程根本找到不到,
所以无法通信, 只能通过子进程复制父进程的方法,让自己从能够访问到相同的管道,来实现通信。
(管道操作:io操作---文件描述符)
半双工:单向通信(所以必须确定方向)
原理:
匿名管道的原理其实就是一个创建一个子进程,子进程复制了父进程的描述符表,因此也有两个描述符,并且它们指向同一个管道,这时候它们两个都能访问到这个管道,因此可以通信。
只不过因为管道是半双工单向通行,因此在通行之前需要确定数据流向,
#include<unistd.h>
功能:创建无名管道
原型:int pipe(int fd[2]);
参数:文件描述符数组。其中fd[0]表示读端,fd[1]表示写端
返回值:成功返回0,失败返回错误代码
代码实现
//这是一个匿名管道的demo
//父进程写子进程读
//就能用于具有亲缘关系的进程间通信
//创建匿名管道必须在创建子进程之前
//否则子进程将无法复制
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
int main()
{
pid_t pid;
int pipefd[2] = {0};
if(pipe(pipefd) < 0)
{
perror("pipe");
return -1;
}
pid = fork();
if(pid < 0)
{
perror("error");
return -1;
}
else if(pid == 0)
{
sleep(1);
close(pipefd[1]);
char buff[1024] = {0};
read(pipefd[0],buff,1024);
printf("child:%s\n",buff);
close(pipefd[0]);
}
else
{
close(pipefd[0]);
char *ptr = "wjf";
write(pipefd[1],ptr,strlen(ptr));
close(pipefd[1]);
}
return 0;
}
匿名管道特性
- 只能用于具有亲缘关系的进程间通信
- 管道是半双工的单向通信
- 管道的生命周期随进程(打开管道的所用进程退出,管道释放)
- 管道是面向字节流传输数据的(面向字节流:数据无规则,无明显边界,收发灵活,可能会粘连)
- 自带同步与互斥
同步:保证一个操作的访问时序性(我操作完了你在操作)
互斥:对临界资源同一时间的唯一访问性,保护临界资源的安全(我操作时你不能操作)
实现管道符 |
| 为匿名管道
//这是一个实现管道符的demo
//命令:ps -ef | grep ssh
//一个进程运行ps程序,一个进程运行grep程序
//ps程序就需要将结果通过管道传递给grep程序
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
int main()
{
pid_t pid = -1;
int pipefd[2] = {0};
if(pipe(pipefd) < 0)
{
perror("pipe");
return -1;
}
pid = fork();
if(pid < 0)
{
perror("fork");
return -1;
}
else if(pid == 0)
{
//子进程运行grep程序处理ps的结果(从管道读数据)
// sleep(3);
dup2(pipefd[0],0);//重定向:从管道中获取
close(pipefd[1]);
execl("/bin/grep","grep","ssh",NULL);
close(pipefd[0]);
}
else
{
//父进程运行ps程序,将结果写入管道
dup2(pipefd[1],1);//重定向:本身将数据写入标准输出显示器。重定向后写入管道
close(pipefd[0]);
execl("/bin/ps","ps","-ef",NULL);
close(pipefd[1]);
}
return 0;
}
命名管道
命名管道:文件系统可见,是一个特殊(管道类型)类型文件(其他进程都能看见,所以任意进程都可以打开)
命名管道可以应用于同一主机上的所有进程间通信
创建:
命名创建:mkfifo pipe_filename
代码创建: int mkfifo(const char*pathname, mode_t mode )
头文件: #include <sys/types.h>
#include <sys/stat.h>
这两个头文件已经在#include<unistd.h>中包含过了
p开头文件为管道文件
命名管道特性
- 不仅具有匿名管道的读写特性,并且还有自己的打开特性
- 匿名管道是直接打开的,pipe直接返回的文件描述符
- 命名管道创建之后,并不会直接打开,需要用户自己open打开,后续通过文件描述符操作
- //打开管道文件:
- //如果以只读打开命名管道,那么open函数将阻塞等待
- //直到其他进程以写的方式打开这个命名管道
- //如果以只写的打开命名管道,那么open函数将阻塞等待
- //直到有其它进程以读的方式打开这个秘密管道
- //如果命名管道以读写的方式打开,则不会阻塞
代码实现
:命名管道实现进程间交流
至少一写一读所以有两份代码:
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>
int main()
{
char *file = "./test.fifo";
umask(0);//仅仅在当前进程有效
if(mkfifo(file,0664) < 0)
{
if(errno == EEXIST)
printf("fifo exit\n");
else
{
perror("mkfifo");
return -1;
}
}
int fd =open(file,O_RDONLY);//只能在mkfifo处创建,不能使用O_CREAST
if(fd < 0)
{
perror("open");
return -1;
}
printf("open fifo success!\n");
while(1)
{
char buff[1024] = {0};//如果不初始化直接定义,结尾可能不是'\0',都是乱码 (memset(buff,0x00,1024)也可以)
int ret = read(fd,buff,1024);
if(ret > 0 )
printf("wjf say:%s\n",buff);
}
return 0;
}
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>
int main()
{
char *file = "./test.fifo";
umask(0);//仅仅在当前进程有效
if(mkfifo(file,0664) < 0)
{
if(errno == EEXIST)
printf("fifo exit\n");
else
{
perror("mkfifo");
return -1;
}
}
int fd =open(file,O_WRONLY);//只能在mkfifo处创建,不能使用O_CREAST
if(fd < 0)
{
perror("open");
return -1;
}
printf("open fifo success!\n");
while(1)
{
fflush(stdout);
char buff[1024] = {0};
printf("input: ");
scanf("%s",buff);
write(fd,buff,strlen(buff));
}
return 0;
管道读写规则
1:管道无数据:读取
————如果描述符是默认的阻塞属性,读取将会阻塞挂起等待,直到管道有数据
————如果描述符被设置为非阻塞属性,读取操作符将不具备条件,直接报错返回EAGAIN
2:管道数据存满
————如果描述符是默认的阻塞属性,写入操作将会阻塞挂起等待,直到有数据被进程取走
————如果描述符被设置为非阻塞属性,写入操作将不具备条件,直接报错返回EAGAIN
3: 如果写端全部被关闭,这时候如果读取数据,读取完管道中的数据,然后返回0。
4: 如果读端全部被关闭,这时候如果写入数据,则会触发异常,操作系统会给进程发送SIGPIPE信 号,进程收到这个信号将会退出
5: 当写入的数据大小超过PIPE_BUF,则这个操作是一个非原子操作,有可能被打断
6:当写入的数据大小小于PIPE_BUF,管道保证读写的原子性(操作不会被打断,造成数据混乱)
操作系统中ipc的相关命令
ipcs:查看ipc信息
-q:查看消息队列
-m: 查看共享内存
-s:查看信号量
ipcrm 删除ipc
共享内存(重点)
一般情况,写入数据将数据从用户空间拷贝到内核空间,读取数据时将数据从内核空间拷贝到用户空间,效率低下
共享内存是最快的IPC形式,一旦这个月的内存映射到共享它的进程空间,这些进程间的数据不在涉及到内核
换句话说进程不在通过执行进入内核的系统调用来传递彼此的数据
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
进程间通信最快的一种:多个进程将同一块物理内存映射到自己的虚拟地址空间,以这种方式’操作这个虚拟地址
相较于其他通信方式,少了两步用户空间和内核空间的拷贝过程因此速度最快
- 创建/打开一块共享内存
- 将这块共享内存映射到自己的虚拟地址空间
- 各种内存的操作
- 解除映射关系
- 删除共享内存 ipcs -m 、ipcs -m shmid
生命周期:
删除一个共享内存的时候,如果这个共享内存依然有其他进程映射链接,这时候这个共享内存不会被直接删除,而是等到所有进程都与共享内存解除映射之后才删除
代码实现
//这是一个共享内存的demo 共享数据
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
#define IPC_KEY 0x123456
int main()
{
shmget(IPC_KEY,333,IPC_CREAT | 0664);
return 0;
}
代码实现获取数据
//这是一个共享内存的demo 共享数据
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
#define IPC_KEY 0x123456
int main()
{
int shmid = -1;
shmid = shmget(IPC_KEY,333,IPC_CREAT | 0664);
if(shmid < 0)
{
printf("shmget error\n");
return -1;
}
void *shm_start = shmat(shmid,NULL,0);
if(shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
int i = 0;
while(1)
{
printf("魔镜魔镜谁最帅? %s",shm_start);
sleep(1);
}
return 0;
}
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
#define IPC_KEY 0x123456
int main()
{
int shmid = -1;
// int shmget(key_t key, size_t size, int shmflg);
// key:共享内存在系统上的标识
// ftok这个接口可以通过一个文件计算出一个key值
//size:共享内存的大小
//shmflg:IPC_CREAT 创建 | 权限
//返回值:共享内存的操作句柄
shmid = shmget(IPC_KEY,333,IPC_CREAT | 0664);
if(shmid < 0)
{
printf("shmget error\n");
return -1;
}
//创建的这个共享内存无法直接操作,因为我们只能操作虚拟地址空间中的地址 //因此第二步就是将共享内存映射到虚拟地址空间,让我们能够通过虚拟地址来访>问这块内存 // void *shmat(int shmid, const void *shmaddr, int shmflg);
//shmid:共享内存句柄
//shmaddr:映射首地址(通常置空,交给操作系统取判断)
//shmflg: SHM_RDONLY 只读否则可读可写
//返回:映射到虚拟地址空间的首地址,失败:(void*) -1
void *shm_start = shmat(shmid,NULL,0);
if(shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
int i = 0;
while(1)
{
sprintf(shm_start,"%s----%d\n","of course 王佳飞",i++);
sleep(1);
}
return 0;
消息队列
是内核为我们创建的一个队列,通过这个队列的标识符key,每一个进程都可以打开这个队列,每一个进程都可以通过这个队列中插入一个节点或者插入一个节点来完成不同进程间的通信。这个队列中的节点有一个类型。)(有类型的数据块)
ipcs -q(查看消息队列)
- 在内核中创建一个消息队列,其他的所有进程都可以通过相同的IPC_KEY打开消息队列
- 这时候可以向队列中添加数据,也可以拿数据(全双工)
- 但是可能存在拿错的问题,所以能够放的数据是有类型的数据块
- 并且读写的时候只能按消息块来发送或接受
ipcs -q 通过这个命令和选项来查看操作系统上的消息队列
ipcrm -q msgid 通过这个命令删除指定队列消息
- 创建消息队列:msgget :创建
- 发送数据/接受数据:msgsnd:发送 msgrcv:接受
- 释放消息队列: msgctl:控制
- 消息队列生命周期随内核
- msgrcv:msgtype
- msgtype:取队列中第一个节点,不分类型
- msgtype>0:取指定类型数据块的一个节点
- msgtype<0:取小于msgtype绝对值类型的第一个节点
信号量
信号量是进程间的通信方式之一,但是并不是用来进行数据传输,而是用来进程控制(进程间的同步与互斥)
保证进程间对临界资源的安全有序访问,同步保证有序,互斥保证安全。(具有等待队列的计数器)
- 同步:保证对临界资源访问的时序可控性
- 互斥:对临界资源同一时间的唯一访问性
多个进程同时操作一个临界资源的时候就需要通过同步与互斥的机制来实现临界资源的安全访问。
- 本质:具有一个等待队列的计数器!(代表现在还有没有资源使用)
当计数器不大于0时,代表没有资源可用时,需要阻塞等待。 - 同步:
只有信号量资源计数从0转变为1时,会通知别人中断阻塞等待
在去操作临界资源。也就是说资源释放(+1)之后其他进程才能获取资 源 ···(-1),然后进行操作。
可以理解为停车场的可用车位计数牌,走一辆车多一个空位所以+1,反之-1 - 互斥:
信号量如果想要实现互斥,那么它的计数器只能是0或1(一元信号量)
我获取计数器的资源,那么别人就无法获取。
信号量的操作:
semget 创建信号量
semctl SETVAL SETALL 设置信号量的计数器初始值
在对临界资源操作前先获取信号量 -1操作
对临界资源操作完毕之后需要释放信号量+1操作
semop
semctl
信号量作为进程间通信方式,意味着大家都能够访问到信号量,信号量实际上也是一个临界资源,当然信号量的这个临界资源是不会出问题的,为什么?
因为信号量的操作是一个原子操作
信号量的操作:
semget 创建信号量
semctl SETVAL SETALL 设置信号量的计数器初始值
在对临界资源操作前先获取信号量 -1操作
对临界资源操作完毕之后需要释放信号量+1操作
semop
1:在对临界资源操作前先获取信号量 -1操作
2:对临界资源操作完毕之后需要释放信号量+1操作
semctl IPC_RMID
:删除