目录
一、进程通信
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 IPC System 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" << endl;return 1;}cout << "fd[0]: "< 模拟父子进程在管道读写通信:
#include
#include #include #include #include #include using namespace std;int main() {//1.创建管道int pipefd[2] = {0};if (pipe(pipefd) != 0){cerr << "pipe" << endl;return 1;}// 2.创建子进程pid_t id = fork();if (id < 0){cerr << "fork error" << endl;return 2;}else if( id == 0){//child//子进程来进行读取,子进程就应该关掉写端close(pipefd[1]);#define NUM 1024char buffer[NUM];while(true){memset(buffer,0,sizeof(buffer));ssize_t s = read(pipefd[0],buffer,sizeof(buffer)-1);if(s>0){//读取成功buffer[s] = '\0';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 #include using namespace std;typedef void (*functor)(); // 函数指针 vector functors; // 方法集合 // for debug unordered_map info;void f1() {cout << "这是一个处理日志的任务,执行的进程 ID [" << getpid() << "]"<< "执行时间是[" << time(nullptr) << "]" << endl; }void f2() {cout << "这是一个备份任务,执行的进程 ID [" << getpid() << "]"<< "执行时间是[" << time(nullptr) << "]" << endl; } void f3() {cout << "这是一个处理网络连接的任务,执行的进程 ID [" << getpid() << "]"<< "执行时间是[" << time(nullptr) << "]" << endl; }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" << endl;return 1;}// 2.创建子进程pid_t id = fork();if (id < 0){cerr << "fork error" << endl;return 2;}else if (id == 0){// 3.关闭不需要的文件fd// child,readclose(pipefd[1]);// 4.业务处理while (true){uint32_t operatorType = 0;// 如果有数据就读取。如果没有数据,就阻塞等待,等待任务的到来ssize_t s = read(pipefd[0], &operatorType, sizeof(uint32_t));if(s == 0){cout<<"需求已完成,我即将退出服务状态!"<
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 #include using namespace std;typedef void (*functor)(); // 函数指针 vector functors; // 方法集合 // for debug unordered_map info;void f1() {cout << "这是一个处理日志的任务,执行的进程 ID [" << getpid() << "]"<< "执行时间是[" << time(nullptr) << "]\n" << endl; }void f2() {cout << "这是一个备份任务,执行的进程 ID [" << getpid() << "]"<< "执行时间是[" << time(nullptr) << "]\n" << endl; } void f3() {cout << "这是一个处理网络连接的任务,执行的进程 ID [" << getpid() << "]"<< "执行时间是[" << time(nullptr) << "]\n" << endl; }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 pair elem; //第一个int32_t:进程pid,第二个int32_t:该进程对应的管道写端fd vector assignMap; int processNum = 5;void work(int blockFd) {cout << "进程[" << getpid() << "]" << "开始工作" << endl;// 子进程核心工作的代码while (true){// a.阻塞等待 b.获取任务uint32_t operatorCode = 0;ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));if (s == 0)break;assert(s == sizeof(uint32_t));(void)s;// c.处理任务if (operatorCode < functors.size())functors[operatorCode]();}cout<<"进程["< &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 << "父进程指派任务——>" << info[task] << "给进程:"<< processFds[pick].first << "编号:" << pick << endl;} } int main() {loadFunctor();vector assignMap;// 创建processNum个进程for(int i =0; i< processNum;i++){//定义保存管道上fd的对象int pipefd[2] = {0};//创建管道pipe(pipefd);//创建子进程pid_t id = fork();if( id == 0){//子进程读取close(pipefd[1]);//子进程执行work(pipefd[0]);close(pipefd[0]);exit(0);}// 父进程做的事情close(pipefd[0]);elem e(id, pipefd[1]);assignMap.push_back(e);}cout<<"Create All Process Success!"< 0){cout << "Wait for: pid=" << assignMap[i].first << "Wait Success!"<< "number:" << i << endl;}} } 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 != '\0') {if (!isspace(*ptr)) {argv[argc++] = ptr;while((!isspace(*ptr)) && (*ptr) != '\0') {ptr++;}continue;}*ptr = '\0';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 != '\0') {if (*ptr == '>') {*ptr++ = '\0';redirect_type++;if (*ptr == '>') {*ptr++ = '\0';redirect_type++;}while(isspace(*ptr)) {ptr++;}file = ptr;while((!isspace(*ptr)) && *ptr != '\0') {ptr++;}*ptr = '\0';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 != '\0') {if (*ptr == '|') {pipe_num++;*ptr++ = '\0';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 filename int mkfifo(const char *filename,mode_t mode);int main(int argc, char *argv[]) {mkfifo("p2", 0644);return 0; }
11.2 匿名管道与命名管道的区别
匿名管道由pipe函数创建并打开。 命名管道由mkfififo函数创建,打开用open FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。11.3 命名管道的打开规则
如果当前打开操作是为读而打开FIFO时如果当前打开操作是为写而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_NONBLOCK enable:立刻返回成功
- 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语言实现)
serverPipe.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 serverPipe
clientPipe.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; } 结果:#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 serverFifo
serverFifo:接收端行间距有空格是因为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 .fifo
serverFifo.cpp:
#include "comm.h" using namespace std;// 读取 int main() {// extern int errno;umask(0);if (mkfifo(IPC_PATH, 0600) != 0){cerr << "mkfifo error" << endl;return 1;}// cout<<"hello,server"<
0){buffer[s] = '\0';cout << "客户端——>服务器#" << buffer << endl;}else if (s == 0){cout << "客户退出,服务器也将退出!";break;}else{cout << "read:" << strerror(errno) << endl;}}close(pipeFd);cout << "服务端退出!" << endl;unlink(IPC_PATH); // 删除创建的管道文件 .fiforeturn 0; } clientFifo.cpp:
#include "comm.h" using namespace std;// 写入 int main() {int pipeFd = open(IPC_PATH, O_WRONLY);if (pipeFd < 0){cerr << "open:" << strerror(errno) << endl;return 1;}// cout<<"hello,client"<
上一篇:什么是用户增长? (超详细)
下一篇:哈希表及其与Java类集的关系