目录
二.system V IPC通信方式 & POSIX IPC通信方式
一.继承原始的unix通信
管道(实质是管道文件操作)
1.有名管道 FIFO
用于没有亲缘关系的进程之间的通信,而无名只能用于有亲缘关系的进程
代码实现
创建有名管道方法:mkfifo()
函数原型 : int mkfifo(const char *filename, mode_t mode);
filename :管道文件的路径 相对路径 绝对路径
mode : 文件的访问权限
返回值 :成功 0
失败 -1 errno perror()
首先创建一个myfifo
mkfifo myfifo
创建输入,输出文件,touch w_mkfifo.c r_mkfifo.c
w_mkfifo.c
#include <stdio.h>
#include <error.h>
#include <time.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd,i;
time_t td;
char buf[1024]= {0};
fd = open("/home/zhangtao/桌面/myfifo",O_WRONLY);//给出路径和权限
if(fd < 0)//判断打开文件情况
{
perror("open error ");
return -1;
}
for(i =0;i<10;i++)//这里我们使用time来进行写入,十次
{
time(&td);
struct tm *tmp;
tmp = localtime(&td);//获取本地时间
strftime(buf,sizeof(buf),"%Y-%m-%d %H:%M:%S",tmp);//写入到buf中
printf("%s\n",buf);//打印时间
write(fd,buf,1024);//将buf中的时间,写入到fd中
sleep(1);//这里休眠1s方便我们后续观察同时运行r和w的情况
}
close(fd);
return 0;
}
r_mkfifo.c
#include <stdio.h>
#include <error.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
int fd;
char buf[1024] = {0};
fd = open("/home/zhangtao/桌面/myfifo",O_RDONLY);
if(fd < 0)
{
perror("open error ");
exit(1);
}
int ret = 0;//定义一个用于接收返回值的ret
while((ret = read(fd,buf,1024)) > 0)//当读到内容就打印buf
{
printf("%s\n",buf);
}
close(fd);
return 0;
}
注:
当我们执行 ./w 或者 ./r 时,会出现阻塞状况,也就是一直停留在黑框框状态
这里存在如下问题:
- A) 对于 FIFO,需要 open 去打开 FIFO 的读端或是写端的描述符(都没有指定O_NONBLOCK 标志的情况下),阻塞情况:
1、如果 open 的是读端时,如果不存在此 FIFO 的已经打开的写端时,open 会一直阻塞到有 FIFO 的写端打开; 如果已经存在此 FIFO 的打开的写端时,open 会直接成功返回。
2、如果 open 的是写端时,如果不存在此 FIFO 的已经打开的读端时,open 会一直阻塞到有 FIFO 的读端打开; 如果已经存在此 FIFO 的打开的读端时,open 会直接成功返回。
- B) 使用open后的fd(没有指定O_NONBLOCK 标志),进行read的阻塞情况:
1、如果存在此 FIFO 或管道的已经打开的写端时,阻塞到 FIFO 或管道中有数据或者 FIFO 或管道的已经打开的写端全部被关闭为止。
2、如果不存在此 FIFO 或管道的已经打开的写端时,read 会直接返回 0;重点来了,此次是非阻塞的行为!
也就是说我们执行./w 时由于./r没有执行,他会一直等待./r 执行,所以我们需要同时执行
此时就在写入时间的同时,也读出了时间。
接下来又有问题了
当我们把代码修改成这样,让read只读一次时,会发生什么呢?
#include <stdio.h>
#include <error.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
int fd;
char buf[1024] = {0};
fd = open("/home/zhangtao/桌面/myfifo",O_RDONLY);
if(fd < 0)
{
perror("open error ");
exit(1);
}
int ret = 0;
ret = read(fd,buf,1024);
printf("%s\n",buf);
close(fd);
return 0;
}
运行之后我们发现,read执行一次之后,后面的write也都不执行了
这其实是因为,我们write时,因为有一个read在读,所以才得以继续,就像一个人在管道里灌水,一个人接水,当接水的人不接了时,灌水的人还在继续灌就会导致管道撑裂。这里是一样的,
141就是管道破裂信号,不能再写入了
读端不存在了,此时只要进程调用write就会收到管道破裂信号 SIGPIPE。
SIGPIPI信号是kill命令中第13个,也就是说这个信号,使得进程直接被杀死了,并没有执行我们函数代码中的报错判断。
2.无名管道 pipe
单向通信管道
创建管道--->创建子进程--->关闭父子进程中不需要的端点--->进行字读父写
#include <unistd.h>
#include <stdio.h>
int main()
{
int fd[2];// pipe函数定义中的fd参数
int val;
char buf[1024] = {0};//定义一个缓冲区用于接收数据
//printf("fd[0]=%d,fd[1]=%d\n",fd[0],fd[1]);//测试
val = pipe(fd);//父进程创建管道,传递fd数组指针
if(val < 0)
printf("pipe creat error");
pid_t pid = fork();//fork出子进程,要放在pipe后面,不然如何知道管道连接到子进程
if(pid < 0)
return -1;
if(pid == 0)//判断子进程执行
{
close(fd[1]);//关闭子进程的写
read(fd[0],buf,1024);//子进程读入数据到buf
printf("%s\n",buf);//输出buff
}
if(pid > 0)//判断父进程
{
close(fd[0]);//关闭父进程的读
write(fd[1],"you are my son",20);//父进程写入数据到fd[1]
}
printf("fd[0]=%d,fd[1]=%d\n",fd[0],fd[1]);//打印父子进程的文件描述符,发现是相同的
}
该段代码中只有一个管道,所以需要关闭父进程的读,子进程的写,让父进程只能写,子进程只能读(父亲吵儿子,父亲只吵,儿子只听)
注:这里的打印函数,我们打印的值从3 开始,因为默认打开 0 1 2描述符已经被 输入 输出 和错误 占用,每一次新的描述符都是从之前的所有描述符中,没有被占用的最小位开始。例:我们关闭0和2描述符,那么我们打印出来的就不是 3 4,而是0 2。(如果关闭1 ,就没有了输出,我们看不到屏幕上的结果)
双向通信管道
创建两个管道--->创建子进程--->分别关闭父子进程中两个管道不需要的端点--->进行字读父写,或者父读子写
#include <unistd.h>
#include <stdio.h>
int main()
{
int fd[2];
int fd2[2];
int val,val2;
char buf[1024] = {0};
char buf2[1024] = {0};
//printf("fd[0]=%d,fd[1]=%d\n",fd[0],fd[1]);
val = pipe(fd);
val2 = pipe(fd2);
if(val < 0 || val2 < 0)
printf("pipe creat error");
pid_t pid = fork();
if(pid < 0)
return -1;
if(pid == 0)
{
close(fd[1]);
read(fd[0],buf,1024);
printf("%s\n",buf);
close(fd2[0]);
write(fd2[1],"i am you son",20);
}
if(pid > 0)
{
close(fd[0]);
write(fd[1],"you are my son",20);
close(fd2[1]);
read(fd2[0],buf2,1024);
printf("%s\n",buf2);
}
printf("fd[0]=%d,fd[1]=%d\n",fd[0],fd[1]);
}
该段代码也就是单向通信管道的升级版,多创建了一个管道用来进行子进程的写入和父进程的读出,也就是双向管道通信方式。
信号 signal
原理:信号是对中断的一种模拟,类似于函数调用
- 这是异步通信方式,多个进程各自运行各自的,通过cpu调度进程
我们上面的SIGPIPE就是一种信号,表示管道破裂。
- 信号相关函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler); //void (*handler)(int);
解释:
int signum:指定要捕获的信号对应的编号。可以填编号,也可以填对应的宏。
sighandler_t handler:函数指针,回调函数;
传入一个函数的首地址,代表捕获信号,且该函数的返回值必须是void类型,参数列表必须是int类型
我们可以调用 signal 函数来进行信号处理,跳过系统的默认处理,例如上面的 SIGPIPE信号
void handel(int sig)//我们在上面的write中加入信号函数
{
printf("Oh rev a signal is %d\n",sig);
printf("go on ..\n");
}
int main()
{
signal(SIGPIPE,handel);//信号调用
int fd,i,w_val;
time_t td;
char buf[1024]= {0};
fd = open("/home/zhangtao/桌面/myfifo",O_WRONLY);
if(fd < 0)
{
perror("open error ");
return -1;
}
for(i =0;i<10;i++)
{
time(&td);
struct tm *tmp;
tmp = localtime(&td);
strftime(buf,sizeof(buf),"%Y-%m-%d %H:%M:%S",tmp);
printf("%s\n",buf);
if(w_val = write(fd,buf,1024) < 0)
{
perror("write fifo miss");
return -1;
}
sleep(1);
}
close(fd);
return 0;
执行程序后出现下面情况
可以看到程序执行了 if 判断的内容,程序正常结束,返回-1.
二.system V IPC通信方式 & POSIX IPC通信方式
基于两个版本的三种相同的方式
1.消息队列
创建/打开消息队列 ---> 添加消息/取出消息 ----> 删除消息队列
- 概念:消息队列就是一个消息的链表。可以把消息看作一个记录,具有特定的格式以及特定的优先级。对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息;对消息队列有读权限的进程则可以从消息队列中读走消息。消息队列是随内核持续的。
- 每个消息队列都有一个队列头,用结构struct msg_queue来描述。队列头中包含了该消息队列的大量信息,包括消息队列键值、用户ID、组ID、消息队列中消息数目等等,甚至记录了最近对消息队列读写进程的ID
- 消息队列是随机查询,并不是管道中的先进先出,并且消息队列在一个进程写入时并不需要另一个进程来等待消息读出。
- 当消息队列处理大量数据时,会出现一定的问题,因为这是不断地需要用户进程写入数据到消息队列或读出数据,此时不断地发生用户态和内核态的状态改变,造成频繁的系统调用,消耗更多时间。
2.共享内存
创建/打开共享内存-----> 映射------> 通信------> 解除映射 ----->删除共享内存
概念:所谓共享内存就是使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。其他进程能把同一段共享内存段“连接到”他们自己的地址空间里去。所有进程都能访问共享内存中的地址。如果一个进程向这段共享内存写了数据,所做的改动会即时被有访问同一段共享内存的其他进程看到。
- 可以发现共享内存解决了消息队列的面对大量数据频繁调用的问题
- 共享内存机制则是通过一个映射函数(如Linux提供了内存映射函数mmap) 把要共享的文件内容映射到进程的
虚拟内存
上, 通过对这段内存的读取和修改实现对文件的读取和修改。共享内存分配在用户空间中。所以进程可以以访问内存的方式对文件进行访问
竞态条件和临界区
临界区:访问和操作共享数据的代码段;
竞态条件:当有多个线程同时进入临界区时,执行结果取决于线程的执行顺序;
例:线程1在a.取出sum值,然后b.对sum+1,然后c.写入sum值,假设线程2在线程1a步骤之后同样取出sum值,并分别进行+1计算,写回sum值,可见,线程1和线程2计算的结果都是1,此时sum值为1;假设线程2在线程1写回数据之后,取出sum值,进行+1,写回操作,此时sum值为2;可见,线程的执行相对时间不同,导致了共享资源出现了不同结果
- 也就是当我们的A进程写入共享区后,如果A又读了出来,那么A就得到的是自己的数据。B进程同样是的。为了避免,我们就需要
保护临界区:为了保护临界区,我们可使用互斥量、读写锁等同步措施,保证同一时间只有一个线程能够进入到临界区,或者同一时间只有一个写线程进入到临界区,从而避免产生竞态条件;
- A先写,B再读,A生产,B消费。同一时刻只能有一个读或一个写,那么进程之间如何知道呢?那么就涉及到了信号量的使用,我告诉你我写完了,该你读了,你读完,告诉我你读完了该我写了......
- 当然如果我们只是简单地分别给两个进程设计信号量,那么最初开始时的信号量谁给呢?如果没有那进程不就阻塞了吗,产生了死锁。我在等你给我信号让我写,你在等我给你信号让你读。
- 我们要尽可能的避免死锁的产生。
3.信号量PV操作
创建/打开信号量---> 初始化信号量(执行 1 次)----> P操作 ----> V操作 --->删除
信号量PV操作:这是一种实现进程互斥与同步的有效方法。在PV操作中,P代表"通过",意味着尝试获取某个资源或执行某些操作;而V代表"释放",意味着完成这些操作后释放资源。PV操作涉及到了信号量的处理,其中信号量是一个用来控制多个进程之间访问共享资源的工具。具体来说,如果信号量的值为0,则表明期待的消息还未到来;一旦信号量的值非0,则说明有期待的消息已经被传递或者生成。在利用PV操作来实现进程同步时,会先进行P操作来检查消息是否已准备好,然后进行V操作以发送消息
三.BSD scoket 套接字
常用来实现网络中不同主机之间的进程通信,这里不再过多解释。
四.pipe 函数
man手册中解释道:pipe() 创建一个管道,一个可用于进程间通信的单向数据通道。 数组 pipefd 用于返回两个文件描述符,引用到管道的末端。 pipefd[0] 是指管道的读取端。 pipefd[1] 是指管道的写入端。 写入管道写入端的数据为由内核缓冲,直到从管道的读取端读取。
简单解释就是:pipe函数定义中的fd参数是一个大小为2的一个数组类型的指针。该函数成功时返回0,并将一对打开的文件描述符值填入fd参数指向的数组。失败时返回 -1并设置errno。通过pipe函数创建的这两个文件描述符 fd[0] 和 fd[1] 分别构成管道的两端,往 fd[1] 写入的数据可以从 fd[0] 读出。并且 fd[1] 一端只能进行写操作,fd[0] 一端只能进行读操作,不能反过来使用。要想互相读写就需要我们的双管道通信方式。
五.简单理解fork
一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
fork有三个返回值:
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
fork出错可能有两种原因:
1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
2)系统内存不足,这时errno的值被设置为ENOMEM。