目录
一、进程通信
1.进程间通信介绍
2.进程间通信目的
3.进程间通信发展
4.进程间通信分类
二、管道
1.什么是管道
2.匿名管道
3.用fork来共享管道原理
4.站在文件描述符角度-深度理解管道编辑
5.编程模拟实现父子进程在管道读写通信编辑
6.进程控制:
6.1 父进程控制单个子进程
6.2父进程控制批量子进程
6.3初识负载均衡:
7.站在内核角度-管道本质
7.1例:在minishell中添加管道的实现:
8.管道读写规则
9.管道特点
10.管道的特征总结:
11.命名管道
11.1 创建一个命名管道
11.2 匿名管道与命名管道的区别
11.3 命名管道的打开规则
11.4 例:用命名管道实现文件拷贝
11.5 例:用命名管道实现server&client通信(C语言实现)
12. 用命名管道实现server&client通信(C++实现)
13.模拟实现两个互不相干的进程进行通信:
后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!
——By 作者:新晓·故知
通信之前,需要让不同的进程(进程具有独立性)看到同一份资源(文件、内存块等)
我们要学的进程间通信,不是告诉我们如何通信,而是两个进程如何先看到同一份资源!
资源的不同,决定了不同种类的通信方式!
数据传输:一个进程需要将它的数据发送给另一个进程资源共享:多个进程之间共享同样的资源。通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
管道System V进程间通信POSIX进程间通信
管道匿名管道pipe命名管道System V IPCSystem V 消息队列System V 共享内存System V 信号量POSIX IPC消息队列共享内存信号量互斥量条件变量读写锁
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”管道:提供共享资源的一种手段!2.匿名管道
#include 功能: 创建一无名管道原型: int pipe(int fd[2]);参数: fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端返回值:成功返回0,失败返回错误代码实例代码
例子:从键盘读取数据,写入管道,读取管道,写到屏幕
#include
#include
#include
#include
int main( void )
{int fds[2];char buf[100];int len;if ( pipe(fds) == -1 )perror("make pipe"),exit(1);// read from stdinwhile ( fgets(buf, 100, stdin) ) {len = strlen(buf);// write into pipeif ( write(fds[1], buf, len) != len ) {perror("write to pipe");break;}memset(buf, 0x00, sizeof(buf));// read from pipeif ( (len=read(fds[0], buf, 100)) == -1 ) {perror("read from pipe");break;}// write to stdoutif ( write(1, buf, len) != len ) {perror("write to stdout");break;}}
}3.用fork来共享管道原理
4.站在文件描述符角度-深度理解管道
5.编程模拟实现父子进程在管道读写通信
mypipe:pipe.ccg++ -o $@ $^ -std=c++11
.PHONY:clean
clean:rm -f mypipe
#include
#include
#include
using namespace std;int main()
{int pipefd[2] = {0};if (pipe(pipefd) != 0){cerr <<"pipe" <模拟父子进程在管道读写通信:
#include
#include
#include
#include
#include
#include
using namespace std;int main()
{//1.创建管道int pipefd[2] = {0};if (pipe(pipefd) != 0){cerr <<"pipe" <0){//读取成功buffer[s] = &#39;\0&#39;;cout<<"子进程收到消息,内容是:"< 嘴巴 ——>读//1——>笔 ——>写pid_t res = waitpid(id,nullptr,0);if(res > 0){cout<<"等待子进程成功!"< } (1)当父进程没有写入数据的时候,子进程在等!所以,父进程写入之后,子进程才能read(会返回) 到数据,子进程打印读取数据要以父进程的节奏为主!
(2)父进程和子进程读写的时候,是有一定的顺序性的!
在管道这里,管道是可以被写满的。查看管道信息指令:ulimit -a
管道内部,如果没有数据,reader就必须阻塞等待(read),等管道有数据。
管道内部,如果数据被写满,writer就必须阻塞等待(write),等待管道中有空间。
以上,具有顺序性是因为pipe内部自带访问控制机制!(同步和互斥机制)
(3)我们之前所学的父、子进程各自printf(向显示器写入,显示器也是文件)的时候,会有顺序吗?
没有。这个没有访问控制机制!
(4)阻塞等待的本质:
将当前进程的task_struct放入等待队列中!
6.进程控制:
控制进程根据需求做出响应。
6.1 父进程控制单个子进程
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#includeusing namespace std;typedef void (*functor)(); // 函数指针
vectorfunctors; // 方法集合
// for debug
unordered_mapinfo;void f1()
{cout <<"这是一个处理日志的任务,执行的进程 ID [" <}void f2()
{cout <<"这是一个备份任务,执行的进程 ID [" <}
void f3()
{cout <<"这是一个处理网络连接的任务,执行的进程 ID [" <}void loadFunctor()
{info.insert({functors.size(), "处理日志的任务"});functors.push_back(f1);info.insert({functors.size(), "备份数据任务"});functors.push_back(f2);info.insert({functors.size(), "处理网络连接的任务"});functors.push_back(f3);
}int main()
{// 0.加载任务列表loadFunctor();// 1.创建管道int pipefd[2] = {0};if (pipe(pipefd) != 0){cerr <<"pipe error" <} 6.2父进程控制批量子进程
进程如何控制批量进程呢?
这里进程池机制。那么将给哪一个进程指派?给它指派什么任务呢?通过什么指派呢?(通过管道指派)
shell脚本指令:
while :; do ps axj | head -1; ps ajx |grep mypipe | grep -v grep; sleep 1; done
6.3初识负载均衡:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#includeusing namespace std;typedef void (*functor)(); // 函数指针
vectorfunctors; // 方法集合
// for debug
unordered_mapinfo;void f1()
{cout <<"这是一个处理日志的任务,执行的进程 ID [" <}void f2()
{cout <<"这是一个备份任务,执行的进程 ID [" <}
void f3()
{cout <<"这是一个处理网络连接的任务,执行的进程 ID [" <}void loadFunctor()
{info.insert({functors.size(), "处理日志的任务"});functors.push_back(f1);info.insert({functors.size(), "备份数据任务"});functors.push_back(f2);info.insert({functors.size(), "处理网络连接的任务"});functors.push_back(f3);
}typedef pairelem;
//第一个int32_t:进程pid,第二个int32_t:该进程对应的管道写端fd
vectorassignMap;
int processNum = 5;void work(int blockFd)
{cout <<"进程[" <}
//[子进程的pid,子进程的管道fd]
void balancesendTask(const vector&processFds)
{srand((long long)time(nullptr));while(true){sleep(1);//选择一个进程,选择进程是随机的,没有压着一个进程给任务//较为均匀的将任务给所有的子进程——负载均衡uint32_t pick = rand()% processFds.size();//选择一个任务uint32_t task = rand()%functors.size();//把任务给一个指定的进程write(processFds[pick].second,&task,sizeof(task));//打印对应的提示信息cout <<"父进程指派任务——>" <}
int main()
{loadFunctor();vectorassignMap;// 创建processNum个进程for(int i =0; i 0){cout <<"Wait for: pid=" < } 7.站在内核角度-管道本质
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \
do \
{ \perror(m); \exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{int pipefd[2];if (pipe(pipefd) == -1)ERR_EXIT("pipe error");pid_t pid;pid = fork();if (pid == -1)ERR_EXIT("fork error");if (pid == 0) {close(pipefd[0]);write(pipefd[1], "hello", 5);close(pipefd[1]);exit(EXIT_SUCCESS);}close(pipefd[1]);char buf[10] = {0};read(pipefd[0], buf, 10);printf("buf=%s\n", buf);return 0;
}7.1例:在minishell中添加管道的实现:
# include
# include
# include
# include
# include
# define MAX_CMD 1024
char command[MAX_CMD];
int do_face()
{memset(command, 0x00, MAX_CMD);printf("minishell$ ");fflush(stdout);if (scanf("%[^\n]%*c", command) == 0) {getchar();return -1; } return 0;
}
char **do_parse(char *buff)
{int argc = 0;static char *argv[32];char *ptr = buff;while(*ptr != &#39;\0&#39;) {if (!isspace(*ptr)) {argv[argc++] = ptr;while((!isspace(*ptr)) && (*ptr) != &#39;\0&#39;) {ptr++;}continue;}*ptr = &#39;\0&#39;;ptr++;}argv[argc] = NULL;return argv;
}
int do_redirect(char *buff)
{char *ptr = buff, *file = NULL;int type = 0, fd, redirect_type = -1;while(*ptr != &#39;\0&#39;) {if (*ptr == &#39;>&#39;) {*ptr++ = &#39;\0&#39;;redirect_type++;if (*ptr == &#39;>&#39;) {*ptr++ = &#39;\0&#39;;redirect_type++;}while(isspace(*ptr)) {ptr++;}file = ptr;while((!isspace(*ptr)) && *ptr != &#39;\0&#39;) {ptr++;}*ptr = &#39;\0&#39;;if (redirect_type == 0) {fd = open(file, O_CREAT|O_TRUNC|O_WRONLY, 0664);}else {fd = open(file, O_CREAT|O_APPEND|O_WRONLY, 0664);}dup2(fd, 1);}ptr++;}return 0;
}
int do_command(char *buff)
{int pipe_num = 0, i;char *ptr = buff;int pipefd[32][2] = {{-1}};int pid = -1;pipe_command[pipe_num] = ptr;while(*ptr != &#39;\0&#39;) {if (*ptr == &#39;|&#39;) {pipe_num++;*ptr++ = &#39;\0&#39;;pipe_command[pipe_num] = ptr;continue;}ptr++;}pipe_command[pipe_num + 1] = NULL;return pipe_num;
}
int do_pipe(int pipe_num)
{int pid = 0, i;int pipefd[10][2] = {{0}};char **argv = {NULL};for (i = 0; i <= pipe_num; i++) {pipe(pipefd[i]);}for (i = 0; i <= pipe_num; i++) {pid = fork();if (pid == 0) {do_redirect(pipe_command[i]);argv = do_parse(pipe_command[i]);if (i != 0) {close(pipefd[i][1]);dup2(pipefd[i][0], 0);}if (i != pipe_num) {close(pipefd[i + 1][0]);dup2(pipefd[i + 1][1], 1);}execvp(argv[0], argv);}else {close(pipefd[i][0]);close(pipefd[i][1]);waitpid(pid, NULL, 0);}}return 0;
}
int main(int argc, char *argv[])
{ int num = 0; while(1) { if (do_face() <0)continue;num = do_command(command);do_pipe(num);}return 0;
}shell脚本:
ps ajx | head -1 && ps ajx | grep sleep
总结:
所谓的命令行中的“|”就是匿名管道!
8.管道读写规则
当没有数据可读时
- O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
- O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
- O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
- O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
- 如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
9.管道特点
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。管道提供流式服务
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程
- 一般而言,内核会对管道操作进行同步与互斥
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道10.管道的特征总结:
1.管道只能用来进行具有血缘关系的进程之间,进行进程间通信,常用于父子进程通信。
2.管道只能单向通信(内核实现决定的)(是半双工的一种特殊情况)。
3.管道自带同步机制(pipe满,writer等待。pipe空,reader等待) ——自带访问控制。
4.管道是面向字节流的。先写的字符,一定是先被读取,没有格式边界,需要用户自己来定义区分内容的边界,例如:[sizeof(uint32_t)]等。这里牵涉到网络TCP协议等知识。
5.管道的生命周期跟随进程:管道是文件,进程退出后,曾经打开的文件也会退出。
那么只能父子进程(或血缘)进程之间通信吗?毫不相干的进程进行通信可以吗?
答:可以。使用命名管道。
11.命名管道
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件11.1 创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:命名管道也可以从程序里创建,相关函数有:创建命名管道:$ mkfifo filenameint mkfifo(const char *filename,mode_t mode);
int main(int argc, char *argv[])
{mkfifo("p2", 0644);return 0;
}11.2 匿名管道与命名管道的区别
匿名管道由pipe函数创建并打开。命名管道由mkfififo函数创建,打开用openFIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。11.3 命名管道的打开规则
如果当前打开操作是为读而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
11.4 例:用命名管道实现文件拷贝
读取文件,写入命名管道:
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \
do \
{ \perror(m); \exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{mkfifo("tp", 0644);int infd;infd = open("abc", O_RDONLY);if (infd == -1) ERR_EXIT("open");int outfd;outfd = open("tp", O_WRONLY);if (outfd == -1) ERR_EXIT("open");char buf[1024];int n;while ((n=read(infd, buf, 1024))>0){write(outfd, buf, n);}close(infd);close(outfd);return 0;
}读取管道,写入目标文件:
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while(0)int main(int argc, char *argv[])
{int outfd;outfd = open("abc.bak", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (outfd == -1) ERR_EXIT("open");int infd;infd = open("tp", O_RDONLY);if (outfd == -1)ERR_EXIT("open");char buf[1024];int n;while ((n=read(infd, buf, 1024))>0){write(outfd, buf, n);}close(infd);close(outfd);unlink("tp");return 0;
}11.5 例:用命名管道实现server&client通信(C语言实现)
# ll
total 12
-rw-r--r--. 1 root root 46 Sep 18 22:37 clientPipe.c
-rw-r--r--. 1 root root 164 Sep 18 22:37 Makefile
-rw-r--r--. 1 root root 46 Sep 18 22:38 serverPipe.c
# cat Makefile
.PHONY:all
all:clientPipe serverPipe
clientPipe:clientPipe.cgcc -o $@ $^
serverPipe:serverPipe.cgcc -o $@ $^
.PHONY:clean
clean:rm -f clientPipe serverPipeserverPipe.c
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \
do{\perror(m);\exit(EXIT_FAILURE);\
}while(0)
int main()
{umask(0);if(mkfifo("mypipe", 0644) <0){ERR_EXIT("mkfifo");}int rfd = open("mypipe", O_RDONLY);if(rfd <0){ERR_EXIT("open");}char buf[1024];while(1){buf[0] = 0;printf("Please wait...\n");ssize_t s = read(rfd, buf, sizeof(buf)-1);if(s > 0 ){buf[s-1] = 0;printf("client say# %s\n", buf);}else if(s == 0){printf("client quit, exit now!\n");
exit(EXIT_SUCCESS);}else{ERR_EXIT("read");}}close(rfd);return 0;
}clientPipe.c
#include
#include
#include
#include
#include
#include
#include
#define ERR_EXIT(m) \
do{\perror(m);\exit(EXIT_FAILURE);\
}while(0)
int main()
{int wfd = open("mypipe", O_WRONLY);if(wfd <0){ERR_EXIT("open");}char buf[1024];while(1){buf[0] = 0;printf("Please Enter# ");fflush(stdout);ssize_t s = read(0, buf, sizeof(buf)-1);if(s > 0 ){buf[s] = 0;write(wfd, buf, strlen(buf));}else if(s <= 0){ERR_EXIT("read");}}close(wfd);return 0;
}结果:12. 用命名管道实现server&client通信(C++实现)
在之前我们学习过,两个在内存中的进程在磁盘打开同一文件的情况。这其实就是命名管道的运用。
我们知道,进程间通信的本质是:不同的进程要看到同一份资源。
匿名管道:子进程继承父进程,找到同一个资源!
命名管道:通过一个fifo 文件—>有路径—>具有唯一性—>通过路径找到同一个资源!
13.模拟实现两个互不相干的进程进行通信:
Makefile:
.PHONY:all
all:clientFifo serverFifoclientFifo:clientFifo.cppg++ -o $@ $^ -std=c++11
serverFifo:serverFifo.cppg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -rf clientFifo serverFifoserverFifo:接收端行间距有空格是因为entel键。
comm.h:
#pragma once#include
#include
#include
#include
#include
#include
#include
#include
#include#define IPC_PATH "./.fifo" makefile:
.PHONY:all
all:clientFifo serverFifoclientFifo:clientFifo.cppg++ -Wall -o $@ $^ -std=c++11
serverFifo:serverFifo.cppg++ -Wall -o $@ $^ -std=c++11.PHONY:clean
clean:rm -rf clientFifo serverFifo .fifoserverFifo.cpp:
#include "comm.h"
using namespace std;// 读取
int main()
{// extern int errno;umask(0);if (mkfifo(IPC_PATH, 0600) != 0){cerr <<"mkfifo error" <#define NUM 1024char buffer[NUM];while (true){ssize_t s = read(pipeFd, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = &#39;\0&#39;;cout <<"客户端——>服务器#" < } clientFifo.cpp:
#include "comm.h"
using namespace std;// 写入
int main()
{int pipeFd = open(IPC_PATH, O_WRONLY);if (pipeFd <0){cerr <<"open:" <}