一、进程间通信的目的;
1.传输数据:一个进程将数据发送给其他的进程
2.共享资源:多个进程将共享同样的资源
3.通知事件:一个进程向另一个进程发送消息
4.进程控制:有的进程控制其他进程的执行
二、进程间通信的主要方式:
管道、消息队列、共享内存
三、管道
·是什么:
管道是进程之间连接的”数据流“,本质是内核中的一块缓冲区,供给进程进行读写、数据交换
·什么样子:
管道的符号: |
eg: ps aux|grep …… ps aux列举出系统中的进程信息,通过管道传输给grep进程,grep再过滤出相应进程的信息。
·怎么创建:
通过相关的函数创建:pipe
pipe函数的解释:int pipe(pipefd[2])
参数:pipefd 是文件描述符,数组元素保存的是文件描述符,并且该数组为输出型参数,由pipe函数填充。其中,pipefd[0]是读端、pipefd[1]是写端
返回值:0 管道创建成功、-1 管道创建失败
进程(内核中为task struct 结构体)创建管道,在tsck struct结构体中,有一结构体指针,指向一个 结构体(struct files _struct),该结构体中有一个数组(元素为结构体指针struct file*,该指针指向struct file (保存了文件的相关信息) ) 如下图:
.如何在Linux环境下应用:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
/*
*验证pipe函数的参数
pipe[0],pipe[1]的意义
* */
/*proc/pid/fd中是否有两个文件描述符
*
* */
int fd[2];
printf("fd[0]=%d,fd[1]=%d ",fd[0],fd[1] );
int i=pipe(fd);
if(i<0)
{
perror("pipe ");
return 0 ;
}
printf("fd[0]=%d,fd[1]=%d ",fd[0],fd[1] );
const char* p="Hello Hi ";
write(fd[1],p,strlen(p));
char buf[1024];
int l=read(fd[0],buf,1024 );
printf("l=%d \n",l);
printf("read buf: %s \n ",buf);
while(1)
{
sleep(1);
}
return 0;
实验结果:
可以看到,在调用pipe函数后,fd[]数组中的值发生了改变,所以pipe函数给fd赋值,pipe中参数为输出型参数,且可以通过管道进行读写操作
从该截图可以看出fd中的数为文件描述符fd[0]=3,fd[1]=4
.父子进程利用管道通信
理论:父子进程都能使用管道进行读写操作-->父子进程有管道的读写两端的文件描述符
操作:先创建管道还是子进程?
先建子进程,再建管道(×) 先创建子进程,由于父进程没有创建管道,没有相关的信息,所以子进程copy父进程的PCB时,也没有相应的管道的读写两端文件描述符,因此不能完成通信。
所以应该先创建管道,再创建子进程。
深入:由于父子进程是抢占式执行,所以不能保证其执行顺序,但为什莫实验的结果却每次一致?
有原因:子进程调用的read函数是阻塞函数
#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
/*
*父进程写,子进程读
* */
int fd[2];
int ret=pipe(fd);
int res;
if(ret<0)
{
perror("pipe \n");
return 0;
}
res=fork();
if(res<0)
{
perror("fork \n" );
return 0;
}
else if(res==0 )
{
char buf[1024];
read(fd[0],buf,sizeof(buf)-1 );
printf("i am child read: %s \n ",buf );
}
else
{
sleep(1000);
const char*p="i am father \n";
write(fd[1],p,strlen(p) );
}
return 0;
}
~
通过pstack(),可以看到子进程阻塞了(read_nocalel()),直到管道中有信息,read函数才读,子进程才继续运行
.管道的特性:
1.数据单向流动(写端-->读端,半双工)
2.匿名管道没有标识符,只能有亲缘关系的进程进行通信
3.管道的生命周期跟随着进程,进程一旦退出,管道也被销毁了
4..管道的大小为64k
验证:
#include<unistd.h>
#include<stdio.h>
int main()
{
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
perror( "pipe ");
}
int count=0;
while(1)
{
int res=write(fd[1],"w",1 );
if(res!=1)
{
break;
}
count++;
printf("res: %d,count: %d \n",res,count);
}
printf("size of pipe :%d \n",count);
return 0;
}
~
65536个字节(64k个字节)
5.管道提供字节流的服务,先后两次写入数据之间没有间隔
验证:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
perror("pipe ");
return 0;
}
write(fd[1],"abc",3 );
write(fd[1],"def ",3);
char buf[1024];
read(fd[0],buf,sizeof(buf)-1 );
printf("%s: ",buf );
return 0;
}
~
~
~
数据连续
6.管道的数据是被读走,而不是拷贝了一份。
验证:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(){
/*
* 目标: 验证从管道当中读取数据的时候, 不是拷贝走, 而是直接读走
*
* 方法:
* 1. 创建管道
* 2. 调用write写入内容
* 3. 调用2次read
* 分析两次读到的结果
* */
int fd[2];
int ret = pipe(fd);
if(ret < 0){
perror("pipe");
return 0;
}
write(fd[1], "abcdef", 6);
char buf[4] = {0};
read(fd[0], buf, sizeof(buf) - 1);
printf("[read1 ]: %s\n", buf); //abc
memset(buf, '\0', sizeof(buf));
read(fd[0], buf, sizeof(buf) - 1);
printf("[read2 ]: %s\n", buf); //def
return 0;
}
管道的数据是被读走,读取一次之后,其中的数据就没有了,而不是拷贝了一份。
.管道的size
pipe_size (并非是管道大小)
可以看到pipe_size默认是 512 * 8 =4096 字节,
作用:
当写入的字节<pipe_size时,保证管道的原子性(一个进程写时,其余进程不能写)
**pipe_size 可以修改 :ulimit -p[num]
**原子性的解释:
一个操作过程不能被间断执行,要幺不做、要么做完;
.阻塞属性
读写两端的文件描述符初始的属性为阻塞属性
当write端一直调用写,读端不去读;则管道写满之后write就会阻塞
当read一直读时,当管道没有内容时,read阻塞
没有读取到信息
一直阻塞
.设置非阻塞属性
针对于管道的读写两端的文件描述符而言。
函数接口:
int fcntl(int fd,int cmd,……/*aarg*/)
参数:
fd:要操作的文件描述符 fd[0]、fd[1]
cmd:告知fcntl要执行的操作:
F_GETFL:获取文件描述符的信息
eg:
fcntl(fd[0],F_GETFL)
返回值:文件描述符的属性信息
F_SETFL:设置文件描述符的属性信息,设置新的属性放入到可变化的参数列表中
eg:
fcntl(fd[1],F_SETFL,属性
#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
int main()
{
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
perror("pipe \n");
return 0;
}
int flag=fcntl(fd[0],F_GETFL);
printf("flag:%d \n",flag);
return 0;
}
F_GETFL:
0:属性设置成功
-1:属性设置失败
设置新属性:
eg:fcntl(fd[0],F_SETFL,old|O_NONBLOCK)
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
perror( "pipe ! \n");
}
int f1=fcntl(fd[0],F_GETFL);
printf("before: %d \n",f1);
fcntl(fd[0],F_SETFL,f1|O_NONBLOCK);
int f2=fcntl(fd[0],F_GETFL);
printf( "after: %d\n",f2);
return 0;
}
根据新设置后的结果有两个疑问:
1.为什么要f1 | O_NONBLOCK
2.为什么修改后的结果是2048
根据属性的源码进行查看:
以0开头的数表示八进制
**文件描述符的属性:O_WRONLY\ O_RDWR\O_RDONLY\O_NONBLOCK
文件描述符的属性信息是文件描述符所代表的struct file指针指向的struct file中的文件属性。
O_RDONLY:
只读属性,其值为 0
O_WRONLY:
只写属性,其值为 1
**在系统接口中,文件打开方式的宏,其实是在内核中按照位图的形式进行存储的。
通过系统源码可以看出,文件描述符的属性用二进制表述,进行属性设置时,用或的方式将新旧属性进行运算,从而修改属性。
#include<stdio.h> #include<unistd.h> #include<fcntl.h> int main() { int fd[2]; pipe(fd); int ret=fcntl(fd[0],F_SETFL, O_WRONLY); int flag=fcntl(fd[0],F_GETFL); printf(" flag: %d\n",flag); return 0; }
通过系统源码,我们知道 O_WRONLY的二进制表示为1,所以修改后的文件描述符属性应该是1,但现在显示是0;
fcntl函数可能对管道的两端做了特殊的设置,对于读写的属性信息不能被做出修改,以防错误。
非阻塞属性的验证:
1.将读端设置为非阻塞:
-->写端不关闭,读端一直调用read读
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
/*创建一个子进程进行读取的操作 */
int main()
{
int fd[2];
pipe(fd);
pid_t pid=fork();
if(pid==0)
{
int flag=fcntl(fd[0],F_GETFL);
fcntl(fd[0],F_SETFL,flag|O_NONBLOCK); //读端不阻塞
char buf[1024]={0};
ssize_t size=read(fd[0],buf,sizeof(buf)-1);
perror("read ");
printf("size: \n",size);
}
return 0;
}
执行结果:返回-1,且显示资源不可用
--> 写端关闭,读端一直读取:
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd[2];
pipe(fd);
pid_t pid=fork();
if(pid==0)
{
close(fd[1]);
int flag=fcntl(fd[0],F_GETFL);
fcntl(fd[0],F_GETFL,flag|O_NONBLOCK);
char buf[1024]={0};
ssize_t read_size=read(fd[0],buf,sizeof(buf)-1);
printf("read_size: %d \n",read_size);
perror("read ");
}
else
{
}
return 0;
}
执行结果:并没有报错,读端的read函数,表示没有读取到任何数据。
2.将写端设置为非阻塞:
读端不关闭,一直写,当把管道写满,再次调用write时,函数报错,返回-1
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
/*
*读端不关闭,一直写,当管道写满时,再次调用write时,就会报错
* */
int main()
{
int fd[2];
pipe(fd);
int count=0;
int flag=fcntl(fd[1],F_GETFL);
fcntl(fd[1],F_SETFL,flag|O_NONBLOCK);
int ret=0;
while(count++<70000)
{
ret=write(fd[1],"s",1);
printf("count: %d \n",count);
}
printf("write_ret=%d \n",ret);
perror("error: ");
return 0;
}
执行结束,报错,一直写,最后显示资源暂时不可用,崩溃了(写的进程收到了SIGPIPE信号,导致写端进程的崩溃)
将读端也关闭
//关闭读端,写端非阻塞
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
int main()
{
int fd[2];
pipe(fd);
pid_t pid=fork();
int flag=fcntl(fd[1],F_GETFL);
fcntl(fd[1],F_SETFL,flag|O_NONBLOCK);
if(pid==0)
{
close(fd[0]);
sleep(3);
ssize_t ret=0;
while(ret!=-1)
{
ret=write(fd[1],"s",1);
printf("write_ret=%d \n ",ret);
}
perror("error: ");
}
else
{
// close(fd[1]);
// close(fd[0]);
while(1)
{
sleep(1);
}
}
return 0;
}
~
若关闭父端的读端,其不会输出任何信息,再查看其状态:发现该子进程变成了僵尸进程
将父端的读端不关闭:
可以看到,由于将父子进程的读写端都关闭,向管道的一端写,但另一端没有读出,就会使管道破裂,然后写端进程会收到内核发出的SIGPIPE信号,从而崩溃。
命名管道:
创建的命名管道文件相当于内核缓冲区的标识符,数据是存储在内核中的缓冲区的,管道文件的作用是让不同的进程通过该文件都能找到内核中的缓冲区。
FIFO(first in first output):第一个写入的数据第一个被读出。
接口函数:
![d](https://i-blog.csdnimg.cn/blog_migrate/abcb3236293e4ece305f4513e383be1e.png)
<sys/stat.h>
int mkfifo(const char *pathname,mode_t mode)
参数:
pathname:创建管道文件的路径、文件名
mode_t:该文件的权限,八进制表示
返回值:
0:成功;
-1:失败