为什么要实现进程间的通讯:因为进程都是相互独立的,但是在很多时候我们要完成一项任务需要进程之间是相互协调的(如,进程间的数据传输,数据共享,事件通知,资源共享和进程控制),这时我们就需要使用进程间通讯的技术。进程间常用的通讯方式有:管道,共享映射区,信号,本地套接字,信号量,消息队列。常用的为前四种
1.管道
管道的本质就是内核缓冲区,它是一个伪文件(不占用磁盘空间),如图:
管道分为两部分:读端与写端,它们分别对应两个文件描述符,数据从写段流入,读端流出。在操作管道的进程被销毁后,管道也将被自动销毁。管道默认是阻塞的。
管道的内部实现方式 是一个环形队列,这样就能使管道能够循环利用,由于默认是阻塞的,所以当管道中没有数据的话,从管道中读的进程就会处于阻塞态,直到另一端的进程往管道中放入数据。当管道处于满状态的时候,往管道中放入数据的进程就会处于阻塞态,直到另一端从管道中读取信息。管道的大小默认为4k,并且会根据实际情况来做调整。
工作方式 :半双工(数据传输方向是单向的)
1.1匿名管道
匿名管道适用于有血缘关系的进程
如何创建匿名管道:
- int pipe(int fd[2]);
参数:
fd[0]-传出参数:读端文件描述符
fd[1]-传出参数:写端文件描述符
注意:创建以后需要使用close关闭管道。
使用示例1:父子间进程通讯
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
//创建匿名管道
int fd[2];
int pip = pipe(fd);
if(pip == -1)
{
perror("pipe");
exit(1);
}
//打印读写端文件描述符
printf("read fd = %d\n",fd[0]);
printf("write fd = %d\n",fd[1]);
//fork
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
exit(1);
}
//父进程 ps aux
if(pid > 0)
{
close(fd[1]);//关闭写端
//文件描述符重定义
dup2(fd[0],STDIN_FILENO);//将终端读指向管道读
//调用命令
execlp("grep","grep","bash",NULL);
perror("execlp1");
exit(1);
}
//子进程 写操作
if(pid == 0)
{
close(fd[0]);//关闭读端
//文件描述符重定义
dup2(fd[1],STDOUT_FILENO);
//调用命令
execlp("ps","ps","aux",NULL);
perror("execlp");
exit(1);
}
return 0;
}
使用示例2:兄弟间进程通讯
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
//创建匿名管道
int fd[2];
int pip = pipe(fd);
if(pip == -1)
{
perror("pipe");
exit(1);
}
//打印读写段文件描述符
printf("read fd = %d\n",fd[0]);
printf("write fd = %d\n",fd[1]);
//fork
int i = 0;
for(i = 0;i < 2;++i)
{
pid_t pid = fork();
if(pid == 0)
{
break;
}
}
if(i == 0)//子进程1
{
//子进程1 实现 ps aux
//关闭读端
close(fd[0]);
//文件描述符重定义
dup2(fd[1],STDOUT_FILENO);
//调用命令
execlp("ps","PS","aux",NULL);
perror("execlp 1");
exit(1);
}
else if(i == 1)
{
//子进程2 实现 grep bash
//关闭写端
close(fd[1]);
//文件描述符重定义
dup2(fd[0],STDIN_FILENO);
//调用命令
execlp("grep","grep","bash",NULL);
perror("execlp 2");
exit(1);
}
else if(i == 2)
{
//父进程 关闭读写端 回收子进程资源
close(fd[0]);
close(fd[1]);
//利用waitpid进行资源回收,将wait设置为非阻塞
pid_t wpid;
int status;
while((wpid = waitpid(-1,&status,WNOHANG)) != -1)
{
if(wpid == 0)
{
continue;
}
printf("end child pid = %d\n",wpid);
if(WIFEXITED(status))
{
printf("return value is %d\n",WEXITSTATUS(status));
}
if(WIFSIGNALED(status))
{
printf("kill single is %d\n",WTERMSIG(status));
}
}
}
return 0;
}
管道的读写行为:
给管道中读写数据,使用标准库函数write,read来实现对管道的读写
- 读操作:read();
<1>有数据:
read(fd)—正常读,返回读取的字节数
<2> 无数据:
当写端全部关闭时:read()解除阻塞,返回零。此时相当于读文件读到了尾部
当写端没有全部关闭时:read()阻塞 - 写操作:write()
读端全部关闭时,管道破裂,进程被终止。内核给当前进程发送信号SIGPIPE
读端没有全部关闭时:若缓存区写满了,write阻塞。若缓存区没有写满,write继续写。
使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
//创建匿名管道
int fd[2];
int pip = pipe(fd);
if(pip == -1)
{
perror("pipe");
exit(1);
}
//打印读写段文件描述符
printf("read fd = %d\n",fd[0]);
printf("write fd = %d\n",fd[1]);
//fork
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
exit(1);
}
char buf[64] = "wo ai ni";
//父进程 读操作
if(pid > 0)
{
close(fd[1]);//关闭写端
read(fd[0],buf,sizeof(buf));
}
//子进程 写操作
if(pid == 0)
{
close(fd[0]);//关闭读端
write(fd[1],buf,strlen(buf));
printf("%s\n",buf);
}
return 0;
}
管道默认是阻塞的,那么如何设置非阻塞呢
使用fcntl函数,该函数可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性。要具体了解该函数,参考https://www.cnblogs.com/zxc2man/p/7649240.html
设置方法:
<1> 首先获取原来的flags
<2>然后设置新的flags
int flags = fcntl(fd[0],F_GETFL);//获取原来的flags
flag = flag | O_NONBLOCK;//设置新的状态为非阻塞,| 的意思为 或
fcntl(fd[0],F_SETFL,flags);//完成对fd[0]设置为非阻塞
** 如何查看管道缓冲区大小**
- 命令:ulimit -a
- 函数:long fpathconf(int fd,name);
name为属性名,有很多种,查看管道缓冲区的属性名为:_PC_PIPE_BUF
1.2有名管道
由于匿名管道只能用于解决由血缘关系的进程,因此linux系统提供了有名管道来实现没有血缘关系的进程间的通讯。
有名管道的特点
它也是一个伪文件,在磁盘上大小为0,我们可以使用ls -l命令来看有名管道,如下图:文件类型为p,大小为0
有名管道在内核中对应一个缓冲区,当我们要使用它时,要使用mkfifo命令在磁盘上建立一个文件,或者我们可以使用mkfifo()函数来在在磁盘上建立一个有名管道的文件。
有名管道采用的也是半双工的通信方式。
有名管道的建立
命令:mkfifo 管道名
函数:mkfifo(管道名,权限);
有名管道的I/O操作
使用I/O函数来进行操作:
open/close
read/write
但是不能进行lseek操作
使用示例:完成没有血缘关系的进程间通信
两个进程
read_fifo与write_fifo.实现这两个进程间的通讯:
read_fifo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
/*读管道内容*/
int main(int argc ,char *argv[])
{
if(argc < 2)
{
printf("please input more arguements\n");
exit(1);
}
//判断文件是否可用
int a = access(argv[1],F_OK);
if(a == -1)
{
//没有管道文件的话,创建管道
int re = mkfifo(argv[1],0777);
if(re ==-1)
{
perror("mkfifo");
exit(1);
}else printf("creat fifo success");
}else
printf("this file have existed\n");
//打开文件
int fd = open(argv[1],O_RDONLY);
char buf[64];
while(1)
{
int len = read(fd,buf,sizeof(buf));
printf("data:%s,dataSize= %d\n",buf,len);
}
close(fd);
return 0;
}
write_fifo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
/*写管道内容*/
int main(int argc ,char *argv[])
{
if(argc < 2)
{
printf("please input more arguements\n");
exit(1);
}
//创建管道
int re = mkfifo(argv[1],0777);
if(re ==-1)
{
perror("mkfifo");
exit(1);
}
printf("creat fifo success");
//打开文件
int fd = open(argv[1],O_WRONLY);
char buf[64] = "wo ai ni~~~";
while(1)
{
sleep(1);
write(fd,buf,strlen(buf));
}
close(fd);
return 0;
}
2.内存映射区
使用内存映射区实现进程间通讯,就是可以借助磁盘文件来实现进程间通讯,此时不需考虑进程之间是否具有血缘关系。
存储映射I/O (Memory-mappedI/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。如下图:
这个映射工作可以通过mmap函数来实现。使用mmap可以将磁盘文件的数据映射到内存,用户通过修改内存就可以修改磁盘文件。
mmap函数介绍:
void *mmap(
void *adrr, // 映射区首地址,传NULL
size_t length, // 映射区的大小
//100byte - 4k(缓冲区大小都是4K的整数倍,即不足4k,就等于4k。超过4k,就是8k、16k,………
//不能为0,可以通过lseek(fd,0,SEEK_END)获得
int prot, // 映射区权限
//PROT_READ -- 映射区必须要有读权限
//PROT_WRITE
//ROT_READ | PROT_WRITE
int flags, // 标志位参数
// MAP_SHARED:修改了内存数据会同步到磁盘
//MAP_PRIVATE:修改了内存数据不会同步到磁盘
int fd,// 文件描述符
//干嘛的文件描述符?:要映射的文件对应fd
//怎么得到?:使用open()
off_t offset // 映射文件的偏移量,映射的时候文件指针的偏移量
//必须是4k的整数倍
// 没有特殊需求的情况下,默认指定为0就可以了
);
//返回值:
//--映射区的首地址 - 调用成功
//--调用失败:MAP_FAILED
munmap—释放内存映射区
mmap与munmap的关系就类似于,malloc–free,new–delete的关系。
函数原型:int munmap(void*addr,size_t length);
- addr: 为mmap的返回值,映射区的首地址
- length:mmap的第二个参数,映射区长度
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(void)
{
int fd = open("english.txt",O_RDWR,0777);
if(fd < 0)
{
perror("open");
exit(1);
}
int len = lseek(fd,0,SEEK_END);
void *re = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(re == MAP_FAILED)
{
perror("mmap");
exit(1);
}
printf("%s\n",(char *)re);
close(fd);
munmap(re,len);
}
以上代码为将文件English.txt映射到内存映射区,这样不用通过I/O函数也能对磁盘文件进行操作。
输出结果为,English.txt的内容
思考问题
<1> 如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
- 不能,若对返回值要进行++操作,只需重新定义一个指针即可
<2> 如果open时O_RDONLY, mmap时prot参数指定PROT_READ |PROT_WRITE会怎样?
- 提示权限不够,提示错误:permission denied
- open文件指定的权限应该大于等于mmap第三个参数prot指定的权限
<3> 如果文件偏移量为1000会怎样?
- 移量必须是4096(4K)的整数倍
<4> 如果不检测mmap的返回值会怎样?
- 代码可读性问题
<5> mmap什么情况下会调用失败?
- 二个参数为0
- 第三个参数没有prot_read权限
fd的权限必须与prot_read的值进行权衡 - 移量不是4096的整数倍
<6> 可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以,但是要将文件进行拓展
- 使用ftruncate(fd,int size)
<7> mmap后关闭文件描述符,对mmap映射有没有影响?
- 没有影响
<8> ptr越界操作会怎样?
- 会报错:段错误(操作的非法内存)
使用内存映射区实现进程间通讯
<1> 有血缘关系的:
要知道,父子进程之间是共享内存映射区的,因此可以使用fd作为映射缓冲区,进行通讯。
第二种方法是建立匿名内存映射区进行通讯,方法为:不写open,直接在mmap函数的参数size_t length指定为一个大小为4096倍数的值,将fd指定为-1,将参数flags后面加 | MAP_ANON。
- 补充:父子进程间,共享文件描述符,共性内存映射区
- 注意:兄弟间通讯不能使用匿名内存映射区,要使用有名的,因此第二种方式不能用于兄弟间通信。
- 程序示例:
父子间通讯:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main(void)
{
//创建mmap,实现进程间的通讯,父进程写,子进程将数据打印到终端
int fd = open("english.txt",O_RDWR,0777);
if(fd == -1)
{
perror("open");
exit(1);
}
int len = lseek(fd,0,SEEK_END);
//创建mmap
void *ptr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(1);
}
close (fd);
//fork进程
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
//父进程实现写数据
if(pid > 0)
{
strcpy((char*)ptr,"you~~~");
wait(NULL);//回收子进程
}
//子进程将数据打印到终端
else if(pid == 0)
{
sleep(1);
printf("%s\n",(char *)ptr);
}
munmap(ptr,len);
return 0;
}
兄弟间通讯:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int main(void)
{
//利用mmap进行系兄弟间通信,不能使用匿名mmap只能使用有名的
//首先创建一个磁盘文件,用来当作映射缓冲区
int fd = open("temp",O_RDWR | O_CREAT,0777);
if(fd == -1)
{
perror("open");
exit(1);
}
//建立映射缓存区
void*ptr = mmap(NULL,4096,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(1);
}
//fork
int i = 0;
pid_t pid;
for(;i < 2;++i)
{
pid = fork();
if(pid == 0)
{
break;
}
}
//子进程1进行写数据
if(i == 0)
{
char *data = "sunjiasen";
strcpy((char*)ptr,data);
}
//子进程2将数据写到终端
else if(i == 1)
{
printf("%s\n",(char*)ptr);
}
//父进程回收两个子进程的资源
else if(i == 2)
{
int s;
pid_t wpid;
while((wpid = wait(&s))!=-1)
{
printf("this %dth child\n",wpid);
}
}
close(fd);
munmap(ptr,4096);
return 0;
}
<2> 无血缘关系的进程间通讯
无血缘关系的进程间通讯不能使用匿名映射的方式,只能借助磁盘文件创建映射区进行通讯
示例程序:
进程a与进程b之间通过磁盘文件temp1进行通讯。进程b将数据写入磁盘文件,进程a将磁盘文件中的数据打印到终端。
- 进程a:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main(void)
{
int fd = open("temp1",O_RDWR,0777);
if(fd == -1)
{
perror("open");
exit(1);
}
int len = lseek(fd,0,SEEK_END);
void *ptr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(1);
}
//将数据读到终端
while(1)
{
printf("%s\n",(char *)ptr);
sleep(1);
}
close(fd);
munmap(ptr,len);
return 0;
}
- 进程b:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
int main(void)
{
int fd = open("temp1",O_RDWR | O_CREAT,0777);
if(fd == -1)
{
perror("open");
exit(1);
}
ftruncate(fd,256);//利用open创建文件时,采用该函数给文件扩容
int len = lseek(fd,0,SEEK_END);
void *ptr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(1);
}
//将数据写如映射区
char *data = "hello";
while(1)
{
sleep(1);
strcpy((char*)ptr ,data);
}
close(fd);
munmap(ptr,len);
return 0;
}
有关使用信号进行进程间通讯,明天再总结。