引言:
我们都知道,进程运行时是具有独立性的,要让两个进程进行通信是一件很困难的事情。因此两个进程通信的前提条件是,需要让两个进程看到同一份资源(物理内存)。
进程通信分类
管道:
- 1.匿名管道pipe(有“亲情”关系(多用于父子进程)的进程进行通信)
- 2.命名管道(实现不相关进程之间的通信)
System V IPC
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
管道
- pipe函数:
头文件: #include<unistd.h>
原型:int pipe(int fd[2])
参数:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
成功,返回0。错误返回错误码
- 工作原理
- 父进程读写端一起打开文件(管道)
- 用fork创建子进程(子进程拷贝父进程代码,一样由读写段打开文件)
- 父子进程依照需求,分别关闭读端/写端。
- 让我们看看代码是如何实现匿名管道的:
int main()
{
int fd[2] = {0};
pipe(fd);
pid_t id = fork();
if(id == 0)//子进程写
{
close(fd[0]);//关闭读操作符
char buf[] = "I'm child";
while(1)
{
write(fd[1], buf, strlen(buf));
sleep(1);
}
}
else//父进程读
{
close(fd[1]);//关闭写操作符
char ret[1024];
while(1)
{
ssize_t s = read(fd[0], ret, sizeof(ret) - 1);
if(s < 0)
{
printf("printf error");
break;
}
else if(s > 0)
{
ret[s] = 0;
printf("parent get child : %s\n", ret);
}
else
{
printf("parent get child : %s\n", ret);
break;
}
}
}
return 0;
}
结果:我们可以看到父进程读到了子进程中的信息。
管道读写的规则
关于临界关系的一些概念:
- 1.临界资源:多进程共享的内存资源(管道)
- 2.临界区:访问临界资源的代码
- 3.互斥:任何一个时刻只能由一个人访问临界区
- 4.饥饿问题:一个排在R队列中的进程很久都无法获得资源
- 5.同步:在保证临界资源安全的前提条件下(通常为互斥),让多进程访问临界资源具有一定的顺序性。功能:协同进程步调,避免饥饿问题
- 6.原子性:对于临界资源,要么访问完毕,要么不访问。
管道读写会发生的四种情况:
- 1.读端不读,读端关闭(写端到/0处,出异常)
- 2.写端不写,写端关闭(写端到/0处,出异常)
- 3.写端一直写,读端一直不读(阻塞)
- 4.读端一直读,写端一直不写(阻塞)
让我们看看这四种情况:
- 1.子进程一直在写,父进程不读
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int fd[2] = {0};
pipe(fd);
pid_t id = fork();
if(id == 0)//子进程写
{
close(fd[0]);
int count = 0;
char buf[] = "I'm child";
while(1)
{
write(fd[1], buf, strlen(buf));
printf("%d\n", count += strlen(buf));
}
}
else//父进程不读
{
close(fd[1]);
waitpid(id, NULL, 0);
}
return 0;
}
结果:进程运行到一定程度,停止(被阻塞)
原因:这个进程被阻塞,不能被调度,不能被运行,由R状态-》非R状态,进程被挂起,将PCB挂到等待队列中。
- 2.写端不仅不写,写端写完毕后将描述符关闭。
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int fd[2] = {0};
pipe(fd);
pid_t id = fork();
if(id == 0)//子进程写完后关闭
{
close(fd[0]);
int count = 0;
char buf[] = "I'm child";
while(1)
{
write(fd[1], buf, strlen(buf));
printf("%d\n", count += strlen(buf));
close(fd[1]);
sleep(1);//子进程一秒之后关闭
break;
}
}
else//父进程读
{
close(fd[1]);
char ret[1024];
while(1)
{
sleep(1);
ssize_t s = read(fd[0], ret, sizeof(ret) - 1);
if(s < 0)
{
printf("printf error");
break;
}
else if(s > 0)
{
ret[s] = 0;
printf("parent get child : %s\n", ret);
}
else
{
printf("read file end!\n");
break;
}
}
waitpid(id, NULL, 0);
}
return 0;
}
结果:子进程一秒打完,父进程读取完之后,退出。
- 3.子进程一直写,父进程直接关闭
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int fd[2] = {0};
pipe(fd);
pid_t id = fork();
if(id == 0)//子进程写
{
close(fd[0]);
int count = 0;
char buf[] = "I'm child";
while(1)
{
write(fd[1], buf, strlen(buf));
printf("%d\n", count += strlen(buf));
sleep(1);
}
}
else//父进程读
{
close(fd[1]);
char ret[1024];
while(1)
{
sleep(1);
ssize_t s = read(fd[0], ret, sizeof(ret) - 1);
if(s < 0)
{
printf("printf error");
break;
}
else if(s > 0)
{
ret[s] = 0;
printf("parent get child : %s\n", ret);
}
else
{
printf("read file end!\n");
break;
}
break;
}
close(fd[0]);
int status = 0;
waitpid(id, &status, 0);//获取信号
printf("sign : %d\n", status & 0x7F);//信号再第7位
}
return 0;
}
结果:子进程退出,父进程读取子进程的退出码
原因:子进程被终止,父进程获得子进程的状态
匿名管道的特点:
- 1.只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信
- 2.管道提供的是半双工的服务,是单向的
- 3.管道是面向字节流服务的(连续的,无边界)
- 4.文件生命周期随进程,进程结束了,管道也就释放了。
命名管道
如果我们想在不相关的进程之间交换数据,就需要用到FIFO文件,被称为命名管道。
如何创建匿名管道
- 1.通过mkfifo指令
- 2.通过mkfifo函数实现
函数原型: int mkfifo(const char* filename, mode_t mode) 参数:管道命名,设置权限。
- 实现:
#include<stdio.h>
#include<unistd.h>
int main()
{
mkfifo("ALA", 0644);
return 0;
}
结果:创建了一个管道文件
命名管道与匿名管道的区别:
- 创造方式不同:命名管道由mkfifo函数创建,匿名管道由pipe函数创建
- 打开方式:命名管道通过open打开,匿名管道通过read打开
- 适用范围不同:命名管道可以实现不想管进程间的通信,匿名管道只能实现有亲缘关系进程间的关系。
命名管道打开的规则:
- 1.如果当前操作是为读打开命名管道的:
- O_NONBLOCK disable:阻塞直到有相应进程为写打开该命名管道(FIFO)
- O_NONBLOCK enaable: 立即返回成功
- 2.如果当前操作是为写打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为读打开该命名管道(FIFO)
- O_NONBLOCK enaable: 立即返回失败,错误码为ENXIO
实现服务端和客户端的通信(单向)
server(服务器端)
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
#include<stdio.h>
#include<sys/stat.h>
int main()
{
umask(0);
mkfifo("ALA", 0644);//创建命名管道
int op = open("ALA", O_RDONLY);//打开匿名管道
char buf[1024];
while(1)
{
printf("wait ....");
ssize_t s = read(op, buf, sizeof(buf) - 1);//从管道中读数据
if(s > 0)
{
buf[s] = 0;
printf("server read : %s", buf);
}
else if(s == 0)
{
printf("client quit!\n");
break;
}
else
{
printf("read error!\n");
break;
}
}
close(op);
return 0;
}
client(客户端)
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<fcntl.h>
int main()
{
int op = open("ALA", O_WRONLY);//打开命名管道
char buf[1024];
while(1)
{
printf("Please Enter : ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf) - 1);//从标准输入(键盘中读取数据)
if(s > 0)
{
buf[s] = 0;
write(op, buf, strlen(buf));//读完后,再写入管道中
}
else if(s == 0)
{
printf("client quit!\n");
break;
}
else
{
printf("read error!\n");
break;
}
}
close(op);
return 0;
}
结果:客户端从标准输入(键盘中)读取数据,放入命名管道中。客户端再从命名管道中读取数据,通过Printf打印到电脑屏幕上。