目录
1. 进程间通信介绍
我们的目的:让不同的进程想办法看到一份公共的资源!
1.1 进程间通信的目的:
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.2 进程间通信发展
管道
System V进程间通信
POSIX进程间通信
1.3进程间通信分类
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
2. 匿名管道
什么是管道?
父子进程是两个独立的进程,那么父子通信也是进程通信的一种
匿名管道:
只能单向通信
管道是面向字节流的!
仅限于父子通信
2.1小测试1
下面代码简述:
子进程每隔一秒进行写入内容
父进程不间断的读取内容
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2] = { 0 };
if (pipe(pipefd) != 0) {
perror("pipe error!");
return 1;
}
printf("pipefd[0]:%d\n", pipefd[0]);//3读取
printf("pipefd[1]:%d\n", pipefd[1]);//4写入
//我们想让父进程进行读取,子进程写入
if (fork() == 0)
{
//子进程---写入
close(pipefd[0]);
const char* msg = "hello sakeww!";
while (1) {
write(pipefd[1], msg, strlen(msg));
sleep(1);
}
exit(0);
}
//父进程--read
close(pipefd[1]);
while (1)
{
//我们没有让父进程sleep
char buffer[64] = { 0 };
//zero indicates end of file,如果此时read的返回值是0,意味着子进程关闭文件描述符了
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s == 0) {
printf("child quit ...\n");
break;
}
else if (s > 0) {
buffer[s] = 0;
printf("child say to father# %s\n", buffer);
}
else {
printf("read error...\n");
break;
}
}
return 0;
}
//pipefd[2]:是一个输出性参数!我们想通过这个参数读取到打开的两个fd
//int pipe(int pipefd[2]);
2.2小测试2
下面代码基本解释:
子进程不停顿的写入信息
父进程停顿一秒然后读取信息
接受信息(也就是在屏幕上显示信息)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2] = { 0 };
if (pipe(pipefd) != 0) {
perror("pipe error!");
return 1;
}
printf("pipefd[0]:%d\n", pipefd[0]);//3读取
printf("pipefd[1]:%d\n", pipefd[1]);//4写入
//我们想让父进程进行读取,子进程写入
if (fork() == 0)
{
//子进程---写入
close(pipefd[0]);
const char* msg = "hello sakeww!";
while (1) {
write(pipefd[1], msg, strlen(msg));
//sleep(1);
}
exit(0);
}
//父进程--read
close(pipefd[1]);
while (1)
{
sleep(1);
char buffer[64] = { 0 };
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s == 0) {
printf("child quit ...\n");
break;
}
else if (s > 0) {
buffer[s] = 0;
printf("child say to father# %s\n", buffer);
}
else {
printf("read error...\n");
break;
}
}
return 0;
}
2.3小测试3
下面代码简单解释:
子进程写入内容内容
父进程只是停留一秒,但是不读取内容
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2] = { 0 };
if (pipe(pipefd) != 0) {
perror("pipe error!");
return 1;
}
printf("pipefd[0]:%d\n", pipefd[0]);//3读取
printf("pipefd[1]:%d\n", pipefd[1]);//4写入
if (fork() == 0)
{
//子进程---写入
close(pipefd[0]);
const char* msg = "hello sakeww!";
int count = 0;
while (1) {
write(pipefd[1], "a", 1);
count++;
printf("count:%d\n", count);
}
exit(0);
}
//父进程--read
close(pipefd[1]);
while (1)
{
sleep(1);
}
return 0;
}
65536/1024=64
写满64kb的时候,write就不再写入了,为什么?
因为管道有大小!
当write写满的时候,为什么不写了?
当我们的缓冲区写满的时候,我们还是可以写的,因为可以把之前没有读取的数据进行覆盖,这个是在软件设计上是可以实现的,但是为什么不写了?
因为要让reader来读,
不写了的本质是:我要等对方来读!
管道自带同步机制,原则性写入
当缓冲区写满的时候,我们需要对方来读,然后才能写
经过测试,只有当我们读取到一定大小的内容(4kb),我们才可以继续写入
2.4小测试4
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2]={0};
if(pipe(pipefd)!=0){
perror("pipe error!");
return 1;
}
printf("pipefd[0]:%d\n",pipefd[0]);//3读取
printf("pipefd[1]:%d\n",pipefd[1]);//4写入
//我们想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程---写入
close(pipefd[0]);
const char *msg="hello sakeww!";
int count = 0;
while(1){
write(pipefd[1],msg,strlen(msg));
sleep(10);
}
exit(0);
}
//父进程--read
close(pipefd[1]);
while(1)
{
char c[64]={0};
ssize_t s = read(pipefd[0],&c,sizeof(c));
c[s]=0;
printf("father take:%s\n",c);
}
return 0;
}
输出描述:
开始的时候直接输出前三行
后续每间隔十秒输出一行
当我们实际写入的时候
我们写入缓冲区的内容数据,有数据就读,没有数据就等待
即:自动同步机制
2.5小测试5
下面代码简单描述:
子进程写入一个数据,等待十秒,然后退出
父进程不间断的读取数据,当读取的内容没有时,然后退出
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2]={0};
if(pipe(pipefd)!=0){
perror("pipe error!");
return 1;
}
printf("pipefd[0]:%d\n",pipefd[0]);//3读取
printf("pipefd[1]:%d\n",pipefd[1]);//4写入
//我们想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程---写入
close(pipefd[0]);
const char *msg="hello sakeww!";
int count = 0;
while(1){
write(pipefd[1],msg,strlen(msg));
sleep(10);
break;
}
close(pipefd[1]);
exit(0);
}
//父进程--read
close(pipefd[1]);
while(1)
{
char c[64]={0};
ssize_t s = read(pipefd[0],&c,sizeof(c));
if(s>0){
c[s]=0;
printf("father take:%s\n",c);
}
else if(s==0){
printf("write quit...\n");
break;
}
else break;
}
return 0;
}
.
输出描述:
直接输出前三行
然后等待十秒后,出现第四行,并且退出
2.6小测试6
下面代码简单描述:
子进程不间断的写入
父进程读取一次内容,然后直接退出
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int pipefd[2]={0};
if(pipe(pipefd)!=0){
perror("pipe error!");
return 1;
}
printf("pipefd[0]:%d\n",pipefd[0]);//3读取
printf("pipefd[1]:%d\n",pipefd[1]);//4写入
//我们想让父进程进行读取,子进程写入
if(fork()==0)
{
//子进程---写入
close(pipefd[0]);
const char *msg="hello sakeww!";
int count = 0;
while(1){
write(pipefd[1],msg,strlen(msg));
sleep(10);
}
close(pipefd[1]);
exit(0);
}
//父进程--read
close(pipefd[1]);
while(1)
{
sleep(10);
char c[64]={0};
ssize_t s = read(pipefd[0],&c,sizeof(c));
if(s>0){
c[s]=0;
printf("father take:%s\n",c);
}
else if(s==0){
printf("write quit...\n");
break;
}
else break;
break;
close(pipefd[0]);
}
return 0;
}
当我们的读端关闭,写端还在写入,此时站在OS的层面,是不合理的!
已经没有人读了,你还在写入,本质就是在浪费OS资源,OS会直接终止写入进程!
OS给目标发送信号SIGPIPE!
当子进程异常终止的时候,父进程是可以读取到终止信号的
总结:
管道的四种情况:
- 读端不读或者读的慢,写端要等读端
- 读端关闭写段,写端收到SIGPIPE信号直接终止
- 写端不写或者写的慢,读端要等写端
- 写端关闭,读端读完全部pipe内部的数据然后再读,会读到0,表明读到文件结尾!
匿名管道,五个特点:
a. 管道是一个只能单向通信的通信通道
b. 管道是面向字节流的
c. 仅限于父子通信–具有血缘关系的进程进行进程间通信
d. 管道自带同步机制,原子性写入
e. 管道的声明进程时随进程的!
管道是文件吗?
如果一个文件只被当前进程打开,相关进程退出了(会自动递减struct file的ref引用计数),被打开的文件呢?
会被OS自动关闭!当ref为零时
为了解决匿名管道只能父子通信,引入了命名管道
3. 命名管道
3.1引入
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
问:
我们通常标识一个磁盘文件,我们用什么方案呢?
路径/文件名(具有唯一性吗?具有)
我们可以让A进程在磁盘写入
然后用B进程在磁盘进行读取
.
我们可以理解为:echo这个进程进行了写入,然后cat这个进程进行了读取
问题:
A和B如何看到同一份资源呢?== A和B如何看到并打开同一个文件呢?
路径/文件名 具有唯一性
所以现在有两个需求?
- 存在一个文件:当他被打开时,他的数据不要刷新到磁盘上,而只是在内存中所谓临时数据进行保存;
- 这个数据在磁盘也要有一个文件名,方便两个进程使用路径+文件名的方式看到同一份资源
3.2创建命名管道
3.2.1准备工作
问题:
我们给我们的fifo设置的权限是:0666,为什么给我们生成的是0644?
你在创建的时候,它本身是要收到系统umask限制的
紧接着我们继续执行./server ,系统会让我们不要在执行了
当我们下一次需要执行./server时,我们需要新的fifo,所以,,,
一旦我们具有了一个命名管道,此时,只需要让通信双方按照文件操作即可!
此处推荐使用系统接口,
系统调用接口没有缓冲区,没有用户层缓冲区,这个数据将来调用read,write就是直接在管道文件里面操作,不会有什么中间层干扰,这是一个干净的通信方式
3.2.2制作
client.c
#include"commit.h"//此时两个程序就能看到同一份文件了
#include<string.h>
int main()
{
//此时不用创建fifo,我们只需要获取即可
int fd = open(MY_FIFO,O_WRONLY);//不需要O_CREAT
if(fd<0){
perror("open");
return 1;
}
//业务逻辑
while(1){
printf("请输入# ");
fflush(stdout);
char buffer[64]={0};//建立缓冲区
//先把数据从标准输入拿到我们的client进程内部
ssize_t s = read(0,buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
printf("%s\n",buffer);
//拿到了数据
write(fd,buffer,strlen(buffer));//要不要-1,不需要。因为管道也是文件
}
}
close(fd);
return 0;
}
server.c
#include"commit.h"
int main()
{
if(mkfifo(MY_FIFO,0666)<0){ //管道文件的建立
perror("mkfifo");
return 1;
}
//只需要文件操作即可
int fd = open(MY_FIFO,O_RDONLY);
if(fd<0){
perror("open");
return 2;
}
//业务逻辑,可以进行对应的读写
while(1){//c89 90没有true和false bool类型
char buffer[64]={0};//读取内容
ssize_t s = read(fd,buffer,sizeof(buffer)-1);//-1目的:不想让他将缓冲区打满
//键盘输入的时候,\n也是输入字符的一部分
if(s>0){
//读取成功
buffer[s]=0;
printf("client# %s\n ",buffer);
}
else if(s==0){
//对方关闭
printf("client quit...\n");
break;
}
else{
//error
perror("read");
break;
}
}
close(fd);
return 0;
}
Makefile
在这里插入代码片.PHONY:all
all:client server
# 上两行:当你输入make会同时生成两个可执行程序 client server
client:client.c
gcc -o $@ $^
server:server.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f client server fifo
commit
#pragma once
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>//close
#define MY_FIFO "./fifo"
你在输入过程中,当你输入错误时,backspace是删除不了的
可以用 ctrl+ backspace 删除自己输入错误的
运行图片描述:
先执行./server
后执行./client
此时client框内有个请输入#,server框内是空白行
当你在client框内输入字符时,
回车
client显示 请输入# nihao (下一行)nihao
server显示 client# nihao
…
当你在client 输入: ctrl+c 会直接退出
并且:server 输出 client quit… 并直接退出
3.2.3 加点东西
因为命名管道也是基于字节流的,所以实际上,信息传递的时候,是需要通信双方定制“协议的”
延伸:Linux下有个包是:当你输入一个特定的指令时,你的屏幕上会显示 一个小火车在动
同理:上述也可以执行这样的
3.2.4 再加点
在server.c 里面加上这个
显而易见:当我们输入的时候,reverse窗口会一直不显示,等50秒之后才会显示
一个延伸小问题:
为什么我们之前的pipe叫做匿名管道,为什么现在的fifo叫做命名管道呢?
因为人家有名字,为什么呢?
为了保证不同的进程看到同一个文件,必须有名字!这是他们实现共享的方式
匿名管道为什么叫匿名呢?
这个文件没有名字,
为什么这个文件就可以没有名字呢?
因为他是通过父子继承的方式,看到同一份资源,不需要名字来标识同一份资源!
消息队列
接口认识:
共享内存
引入:
上面的都是基于文件的通信方式
SystemV标准的进程间通信方式
在OS层面专门为进程间通信设计的一套方案
谁设计的?—计算机科学家+程序员
要不要给用户用?—肯定要!
如果要给用户用,那么用什么方式给用户用?
OS不相信任何用户,给用户提供功能的时候,采用系统调用!
SystemV 进程间通信,一定会存在专门用来通信的接口(System call)
就需要有个人,组织机构,等来进行定制标准!
在同一主机内的进程间通信方案:
system V 方案!
进程间通信的本质:先让不同的进程看到同一份资源
system V
- 共享内存
- 消息队列(有点落伍)
- 信号量
前两个以传送数据为主要目的
信号量:以实现进程间同步或互斥为目的
共享内存:
引入:
- 通过某种调用,在内存中创建一份内存空间
- 通过某种调用,让进程“挂接”到这份新开辟的内存空间上!
可以让参与通信的多个进程挂接到同一份物理内存
此时达到了:
让不同进程看到了同一份资源!!!
这种方案就叫做共享内存
当我们不用共享内存的时候:
3. 去关联(去挂接)
4. 释放共享内存
准备工作:
1.在任何一个时间段,OS内可不可能存在多个进程,同时使用不同的共享内存来进行进程间通信呢?
答:有可能,
所以:共享内存在系统中可能存在多份!
我在进行进程间通信的同时,有没有可能我不想通信了,我想释放内存,我在释放内存的时候,别人来通信了,建立共享内存了
OS要不要管理这些不同的共享内存呢?必须要!
那么如何管理呢?先描述,在组织
2.你如何保证,两个或者多个进程,看到的是同一个共享内存呢?
共享内存一定要有一定的标识唯一性的ID,方便让不同的进程能识别统一个共享内存资源!
这个“ID”应该在哪里?
描述共享内存的数据结构中!
这个唯一的标识符,用来进行进程间通信的,本质:让不同的进程看到同一份资源
本质的前提:你需要先让不同的进程,看到同一个ID
这个ID是需要 :由用户自己设定的!
认识接口:
共享内存的整个生命周期
简单测试逻辑
测试:
问题:
信号量
接口认识:
什么是信号量?
管道,匿名or命名,贡献内存,消息队列:都是以传输数据为目的的!
信号量不是以传输数据为目的的!
以通过共享“资源”的方式,来达到多个进程的同步和互斥的目的!
例子:
信号量的本质:是一个计数器,类似int count ;
衡量临界资源中资源数目的
感性认识:
1.什么时临界资源?
凡是被多个执行流同时能够访问的资源就是临界资源!
资源被多个进程同时访问的:
同时向显示器打印,这个显示器就是临界资源。
进程间通信的时候,管道,共享内存,消息队列等 都是临界资源
管道,内部提供了一些保护机制,将临界资源保护起来了
共享内存就是典型的临界资源,没有人保护,所以有可能出现,你写了一半就被我读走了的情况,这叫做数据不一致的问题。
问题:
凡是要进程间通信,必定要引入被多个进程看到的资源(通信需要),同时,也造就了引入一个新的问题,临界资源的问题。
2.什么时临界区?
进程的代码可是有很多的,其中,用来访问临界资源的代码,就叫做临界区
server.c
client.c
3.什么时原子性?
一件事情要么不做,要做就做完,没有中间态。
非原子性?
当我们进行某件事的时候,他有中间过程。
在多进程,父进程+子进程
4.什么是互斥?
在任意一个时刻,只能允许一个执行流进入临界资源,执行他自己的临界区