一、管道
1.1 什么是管道?
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
- 管道的本质就是一块缓冲区
1.2 匿名管道
<1> 匿名管道参数说明
#include <unistd.h>
int pipe(int fd[2]);
-
功能:创建一无名管道原型
-
参数
fd:文件描述符数组其中fd[0]表示读端, fd[1]表示写端(我们可以将0看作一张嘴代表读,1看作一支笔代表写)
-
返回值:成功返回0,失败返回错误代码
上图可以更好的帮助理解pipe函数的功能,当调用pipe函数时,向系统传递一个fd文件描述符数组,其中fd[1]对应写端,将数据塞入管道,fd[0]代表读端,从管道中读取数据
#include<stdio.h> #include<unistd.h> int main() { int fd[2] = {0}; int ret = pipe(fd); printf("ret:%d\n",ret); printf("fd[0]:%d\n",fd[0]); printf("fd[1]:%d\n",fd[1]); return 0; }
<2> fork共享管道原理
首先我们先了解管道的两个特性:
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
- 管道是半双工的,也就是说管道只能进行单向通信,即只能一端写一端读,但我们调用fork函数时,如果父进程写入i am father,那么父进程就需要关闭读端,而子进程读取父进程写入的信息时,子进程就需要关闭写端,如下图所示
代码实例
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
int pipefd[2] = {0};
int ret = pipe(pipefd);
if(ret==-1)
{
perror("pipe");
return 1;
}
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
//父进程读取
if(id > 0)
{
//父进程关闭写文件描述符
close(pipefd[1]);
char buf[64];
while(1)
{
ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("father get a msg:%s\n",buf);
sleep(1);
}
}
}
//子进程写入i am child
if(id == 0)
{
//子进程关闭读文件描述符
close(pipefd[0]);
const char *msg = "i am child";
while(1)
{
write(pipefd[1],msg,strlen(msg));
sleep(1);
}
}
return 0;
}
结果
上述程序子进程每次向管道写入信息,父进程从管道读取并打印
<3> 站在文件描述符角度-深度理解管道
-
父进程调用pipe()创建管道,假设系统分配文件描述符3给fd[0]用于读,文件描述符4给fd[1]用于写
-
父进程调用fork()函数创建子进程,子进程具有和父进程同样的数据,文件描述符的指向也是相同的
-
但是管道具有半双工特征,即只能一端读一端写,因此父子进程需要关闭各自不需要的文件描述符,假设父进程写子进程读,那么父进程关闭fd[0]读端,子进程关闭fd[1]写端
<4> 管道读写规则
-
当没有数据可读时
-
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
-
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> int main() { int pipefd[2] = {0}; int ret = pipe(pipefd); if(ret==-1) { perror("pipe"); return 1; } pid_t id = fork(); if(id < 0) { perror("fork"); return 1; } //父进程读取 if(id > 0) { //父进程关闭写文件描述符 close(pipefd[1]); char buf[64]; while(1) { ssize_t s = read(pipefd[0],buf,sizeof(buf)-1); if(s > 0) { buf[s] = 0; printf("father get a msg:%s\n",buf); } } } //子进程写入i am child if(id == 0) { //子进程关闭读文件描述符 close(pipefd[0]); const char *msg = "i am child"; int count = 0; while(1) { write(pipefd[1],msg,strlen(msg)); printf("write a msg:%d\n",count++); sleep(5); } } return 0; }
-
-
当管道满的时候
-
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
-
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> int main() { int pipefd[2] = {0}; int ret = pipe(pipefd); if(ret==-1) { perror("pipe"); return 1; } pid_t id = fork(); if(id < 0) { perror("fork"); return 1; } //父进程读取 if(id > 0) { //父进程关闭写文件描述符 close(pipefd[1]); char buf[64]; while(1) { ssize_t s = read(pipefd[0],buf,sizeof(buf)-1); if(s > 0) { buf[s] = 0; printf("father get a msg:%s\n",buf); sleep(5); } } } //子进程写入i am child if(id == 0) { //子进程关闭读文件描述符 close(pipefd[0]); const char *msg = "i am child"; int count = 0; while(1) { write(pipefd[1],msg,strlen(msg)); printf("write a msg:%d\n",count++); } } return 0; }
-
-
如果所有管道写端对应的文件描述符被关闭,则read返回0
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> int main() { int pipefd[2] = {0}; int ret = pipe(pipefd); if(ret==-1) { perror("pipe"); return 1; } pid_t id = fork(); if(id < 0) { perror("fork"); return 1; } //父进程读取 else if(id > 0) { //父进程关闭写文件描述符 close(pipefd[1]); char buf[64]; while(1) { ssize_t s = read(pipefd[0],buf,sizeof(buf)-1); if(s > 0) { buf[s] = 0; printf("father get a msg:%s\n",buf); sleep(1); } printf("father get a msg:%d\n",s); sleep(1); } } //子进程写入i am child else { //子进程关闭读文件描述符 close(pipefd[0]); const char *msg = "i am child\n"; int count = 0; while(1) { write(pipefd[1],msg,strlen(msg)); printf("write a msg:%d\n",count++); //读10次后关闭写端 if(count == 10) { close(pipefd[1]); break; } } exit(2); } return 0; }
-
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程 退出
-
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
-
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
<5> 管道的特性与特点总结
管道道的四个特性:
- 如果写端不关闭文件描述符且不写入,读端可能需要长时间阻塞;读取条件不满足时,读取端就要被阻塞(管道为空);
- 当我们实际再进行写入时,如果写入条件不满足我们写入端就要进行阻塞(管道满了);
- 如果写端不但不写入还关闭文件描述符,读端读取完数据后就会读到文件结尾;
- 如果读端关闭,写端进程可能在后续会被进程杀掉。
管道特点:
-
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创
建,然后该进程调用fork,此后父、子进程之间就可应用该管道。 -
管道提供流式服务
-
一般而言,进程退出,管道释放,所以管道的生命周期随进程,管道文件只是标识,删除后依然可以通信
-
一般而言,内核会对管道操作进行同步(没有数据读阻塞,缓冲区写满写阻塞)与互斥
-
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
1.3 命名管道
-
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
-
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
-
命名管道是一种特殊类型的文件
<1> 创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename, mode_t mode);
<2> 匿名管道与命名管道的区别
-
匿名管道由pipe函数创建并打开。
-
命名管道由mkfifo函数创建,打开用open
-
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义。
<3> 命名管道的打开规则
- 如果当前打开操作是为读而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_NONBLOCK enable:立刻返回成功
- 如果当前打开操作是为写而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
<4> 用命名管道实现server&clinet通信
-
构建Makefile文件
.PHONY:all all:server client client:client.c gcc -o $@ $^ server:server.c gcc -o $@ $^ .PHONY:clean clean: rm client server fifo
-
服务器代码
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #define FIFO_FILE "./fifo" int main() { umask(0); if(-1 == mkfifo(FIFO_FILE,0666)) { perror("mkfifo"); return 1; } int fd = open(FIFO_FILE, O_RDONLY); if(fd < 0) { perror("open"); return 1; } else { while(1) { char buf[1024]; ssize_t s = read(fd,buf,sizeof(buf)-1); if(s > 0) { buf[s] = 0; printf("#############################\n"); printf("client#:%s\n",buf); } else { close(fd); printf("server offline!\n"); break; } } } return 0; }
-
客户端代码
#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #define FIFO_FILE "./fifo" int main() { int fd = open(FIFO_FILE,O_WRONLY); if(fd < 0) { perror("open"); return 1; } else { while(1) { printf("Please Input Your Message:"); fflush(stdout); char msg[1024]; //从键盘读取信息 ssize_t s = read(0,msg,sizeof(msg)-1); if(s > 0) { msg[s] = 0; write(fd,msg,strlen(msg)); } } } return 0; }
通过命名管道可以发现管道的本质就是一块缓存
二、共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到 内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
2.1 共享内存示意图
进程间能够实现通信必然需要看到同一份资源,而共享内存就是通过让进程A和B能够同时看到同一块物理内存而实现的进程间通信。而共享内存是将同一块物理内存映射到各个进程虚拟地址空间,可以直接通过虚拟地址访问,相较于其它方式少了两步内核态与用户态之间的数据拷贝因此速度最快,对于一份数据想要通过A传递给B,只要拷贝到进程A的地址空间,共享内存再将这份资源拷贝过来,然后再拷贝给进程B,这样减少了诸多的步骤就可以做到高速高效了。
2.2 共享内存函数
<1> shmget函数(创建共享内存)
<2> shmat函数(关联共享内存)
-
说明
<3> shmdt函数(取消关联)
<4> shmctl函数(删除共享内存)
2.3 用共享内存实现server&client通信
<1> Makefile
.PHONY:all
all:client server
clinet:client.c
gcc -o $@ $^
server:server.c
gcc -o $@ $^
.PHONY:clean
clean:
rm client server
<2> comm.h
#pragma once
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x6666
#define SIZE 4096
<3> server.c
#include"comm.h"
int main()
{
//获取一个唯一标识内存的key值
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
//创建共享内存
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
//关联共享内存
char * addr = shmat(shmid, NULL, 0);
sleep(2);
int count = 0;
while(count++ < 26)
{
printf("client#%s\n",addr);
sleep(1);
}
//取消共享内存
shmdt(addr);
sleep(5);
//删除
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
<4> client.c
#include"comm.h"
int main()
{
//获取一个唯一标识内存的key值
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
//创建共享内存
int shmid = shmget(key,SIZE,0);
//关联共享内存
char * addr = shmat(shmid, NULL, 0);
int i = 0;
while(i < 26)
{
addr[i] = 'A' + i;
i++;
addr[i] = 0;
sleep(1);
}
//取消共享内存
shmdt(addr);
sleep(5);
return 0;
}