IPC进程间通信 interprocess communicate
目录
- 1. IPC概述
- 2. IPC三大通信方式分类
- 2.1 古老的通信方式
- 2.2 IPC对象通信
- 2.3 socket通信
- 3. 管道通信详解
- 3.1 管道基本特性
- 3.2 无名管道
- 3.2.1 无名管道特性
- 3.2.2 无名管道使用流程
- 3.2.3 无名管道练习验证
- 3.3 有名管道
- 3.3.1 有名管道框架
- 3.3.2 有名管道操作函数
- 3.3.3 有名管道注意事项
- 4. 信号通信
- 4.1 信号发送端
- 4.2 信号接收处理
- 4.2.1 信号处理方式
- 4.2.2 信号注册函数
- 5. 练习与作业
- 5.1 基础练习
- 5.2 大作业
1. IPC概述
IPC 进程间通信 interprocess communicate
进程间通信的原理:a和b是独立的,所以要找到一个公共的空间,把信息存储在这个空间上,在内核空间上,开一片内存区域,a和b都可以找到这个公共的区域
2. IPC三大通信方式分类
三大类:古老的通信方式,ipc对象通信、socket通信
2.1 古老的通信方式
无名管道、有名管道、信号(siginal)
2.2 IPC对象通信
system v、BSD、suse fedora、kernel.org、unix
消息队列(用的相对少,这里不讨论)
共享内存
信号量集
2.3 socket通信
网络通信(不同主机之间的通信)
线程信号,posix sem_init
特列:
- 古老的通信方式中信号是唯一的异步通信
- 所有的通信方式中共享内存是唯一的最高效
3. 管道通信详解
根据使用场景来区分:
管道分类:
- 无名管道(pipe):只能给有亲缘关系进程通信
- 有名管道(fifo):可以给任意单机进程通信
3.1 管道基本特性
- 管道是半双工的工作模式
- 所有管道都是特殊的文件不支持定位操作。lseek->> fd fseek ->>FILE*
- 管道是特殊文件,读写使用文件IO。fgets,fread,fgetc,open,read,write,close
管道使用注意事项:
- 读端存在,一直向管道中去写,超过64k,写会阻塞。
- 写端存在,读管道,如果管道为空,读会阻塞。(有数据的话就取出来)
- 管道破裂:读端关闭,写管道。是写端破裂了
- read 0:写端关闭,如果管道没有内容,read 0。
如果
使用框架:
创建管道 → 读写管道 → 关闭管道
代码举例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main(int argc, char **argv)
{int fd[2]={0};int ret = pipe(fd);pid_t pid = fork();if(pid>0){//父进程写管道,不读.所以关闭管道的读段.close(fd[0]);char buf[]="hello";sleep(3);write(fd[1],buf,strlen(buf));exit(0);}else if(0 == pid){//子进程读管道,不写.所以关闭管道的写段.close(fd[1]);char buf[50]={0};// 读阻塞. 父进程会等待一会,在写管道 .read(fd[0],buf,sizeof(buf)); printf("pipe %s\n",buf);exit(0);}else {perror("fork");exit(1);}//system("pause");return 0;
}
3.2 无名管道
3.2.1 无名管道特性
- 亲缘关系进程使用
- 有固定的读写端
3.2.2 无名管道使用流程
创建并打开管道:pipe函数
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建并打开一个无名管道
参数:
- pipefd[0]:无名管道的固定读端
- pipefd[1]:无名管道的固定写端
返回值:成功0,失败-1
注意事项:
- 无名管道的架设应该在fork之前进行。
无名管道的读写:使用文件IO的读写方式
- 读:read()
- 写:write()
关闭管道:close()
管道通信举例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int main(int argc, char **argv)
{int fd[2] = {0};int ret = pipe(fd);pid_t pid = fork();if (pid > 0){//父进程写管道,不读.所以关闭管道的读段.close(fd[0]);int i = 0;char buf[1024] = {0};memset(buf, 'a', sizeof(buf));//这个地方会写阻塞, 管道大小64K for (i = 0; i < 65; ++i){write(fd[1], buf, sizeof(buf));printf("i is %d\n", i + 1);}}else if (0 == pid){//子进程读管道,不写.所以关闭管道的写段.close(fd[1]);int i = 5;while (i--){sleep(1);}}else{perror("fork");exit(1);}// system("pause");return 0;
}
管道破裂的代码举例如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int main(int argc, char **argv)
{int fd[2] = {0};int ret = pipe(fd);pid_t pid = fork();if (pid > 0){//父进程写管道,不读.所以关闭管道的读段.close(fd[0]);char buf[] = "hello";printf("father id:%d\n", getpid());sleep(5); //确保子进程 关闭管道的读段//借助gdb 观测管道破裂write(fd[1], buf, strlen(buf)); // 管道破裂 ,当前进程结束,异常printf("------------\n");}else if (0 == pid){printf("child %d\n", getpid());//子进程读管道,不写.所以关闭管道的写段.close(fd[1]);close(fd[0]); //读段关闭int i = 5;}else{perror("fork");exit(1);}// system("pause");return 0;
}
下面代码为无名管道的单相数据传递,最基础的
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int main(int argc, char **argv)
{int fd[2] = {0};int ret = pipe(fd);pid_t pid = fork();if (pid > 0){//父进程写管道,不读.所以关闭管道的读段.close(fd[0]);char buf[] = "hello";write(fd[1], buf, strlen(buf));exit(0);}else if (0 == pid){//子进程读管道,不写.所以关闭管道的写段.close(fd[1]);while (1){char buf[50] = {0};int ret = read(fd[0], buf, sizeof(buf));// read的返回值,如果为0 代表通信结束if (ret <= 0){printf("通讯结束\n");break;}printf("pipe %s\n", buf);}exit(0);}else{perror("fork");exit(1);}// system("pause");return 0;
}
复制一个二进制文件,然后读取,在父子进程中:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <time.h>
int main(int argc, char **argv)
{int fd[2]={0};int ret = pipe(fd);pid_t pid = fork();if(pid>0){close(fd[0]);int srcfd = open("/home/linux/1.png",O_RDONLY);if(-1 == srcfd ){perror("father open");exit(1);}int num =0;while(1){char buf[4096]={0};int ret = read(srcfd,buf,sizeof(buf));if(ret<=0){time_t tm;time(&tm);printf("发送结束,num %d, %lu\n",num,tm);break;}write(fd[1],buf,ret);num+=ret;}close(srcfd);close(fd[1]);exit(0);}else if(0 == pid){close(fd[1]);int dstfd = open("2.png",O_WRONLY|O_CREAT|O_TRUNC,0666);if(-1 == dstfd ){perror("child open");exit(1);}int num = 0 ;while(1){char buf[4096]={0};int ret = read(fd[0],buf,sizeof(buf));if(ret<=0){time_t tm;time(&tm);printf("读取数据结束,%d, tm:%lu\n",num,tm);break;}write(dstfd,buf,ret);num+=ret;}close(fd[0]);close(dstfd);exit(0);}else {perror("fork");exit(1);}
3.2.3 无名管道练习验证
练习:
设计一个多进程程序,用无名管道完成父子进程间的任意信息传送,包括数字、字符串等。
验证问题:
- 父子进程是否都有fd[0] fd[1]?
- 可以,写fd[1]可以从fd[0]读
- 管道的数据存储方式?
- 队列形式存储,读数据会剪切取走数据不会保留,先进先出
- 管道的数据容量?
- 操作系统建议值:512*8=4k
- 实际测试值:65536byte=64k
- 管道的同步效果?
- 读端关闭不能写(→SIGPIPE异常终止)
- 写端关闭可以读(取决于pipe有无内容,read返回0不阻塞)
- 固定的读写端能否互换?能否写fd[0]能否读fd[1]?
- 不可以,是固定读写端
双向通信实现方案:
pipe();
fork();if(pid>0) {read(file,);write(fd[1]);
}if(0 == pid) {read(fd[0]);write(newfile);
}
3.3 有名管道
有名管道===》fifo==》有文件名称的管道
3.3.1 有名管道框架
创建有名管道 → 打开有名管道 → 读写管道 → 关闭管道 → 卸载有名管道
3.3.2 有名管道操作函数
- 创建:mkfifo
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
功能:创建权限为mode的有名管道文件
参数:
- pathname:路径+名称
- mode:8进制文件权限
返回值:成功0,失败-1
- 打开:open
注意事项:
- 打开方式决定当前进程的读写方式
- 只能是O_RDONLY(固定读端)或O_WRONLY(固定写端)
- 不能使用O_RDWR或O_CREAT
- 读写:使用文件IO
- 读:read(fd-read,buff,sizeof(buff))
- 写:write(fd-write,buff,sizeof(buff))
-
关闭:close(fd)
-
卸载:unlink
int unlink(const char *pathname);
功能:卸载并删除有名管道文件
参数: ptahtname 要卸载的有名管道
返回值:成功0,失败-1
有名管道举例代码:
读操作代码:
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<time.h>
#include<unistd.h>
#include<errno.h>
int main(int argc, char **argv)
{int ret = mkfifo("myfifo",0666);if(-1 == ret){perror("mkfifo");exit(1);}int fd =open("mkfifo",O_WRONLY);if(-1 == fd){perror("open");exit(1);}char buf[50] = {0};read(fd,buf,sizeof(buf));printf("fifo:%s\n",buf);close(fd);//remove("myfifo");//system("pause");return 0;
}
写操作代码:
#include <asm-generic/errno-base.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<time.h>
#include<unistd.h>
#include<errno.h>
int main(int argc, char **argv)
{int ret = mkfifo("myfifo",0666);if(-1 == ret){if(EEXIST!=errno){perror("mkfifo");exit(1);}}int fd =open("mkfifo",O_WRONLY);if(-1 == fd){perror("open");exit(1);}char buf[50] ="hello";write(fd,buf,strlen(buf));//printf("fifo:%s\n",buf);close(fd);//remove("myfifo");//system("pause");return 0;
}
3.3.3 有名管道注意事项
- 同步问题:
- 必须有读写端同时存在
- 如果有一端没有打开,open函数会阻塞
- 亲缘关系进程使用:
- 可以在有亲缘关系的进程间使用
- 注意启动次序可能导致阻塞
- 手工操作:
- 读:cat fifoname
- 写:echo “content” > fifoname
1)明确点:是否需要同步?同步的位置
读写端关闭,是否可以写,不能写的话,是什么原因,写端关闭,是否可以读。
2)结论:有名管道执行过程中必须读写端同时存在,如果有一段没有打开,则默认在open函数部分阻塞
3)、能否手工操作有名管道实现数据的传送。
读: cat fifoname
写: echo “asdfasdf” > fifoname
strcat
举例项目代码:可以实现信息的传递:
#include <errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<time.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
int main(int argc, char **argv)
{int ret = mkfifo("myfifo",0666);if(-1 == ret){if(EEXIST != errno){perror("mkfifo");exit(1);}}int fd = open("myfifo",O_WRONLY);if(-1==fd){perror("open");exit(1);}int srcfd =open("/honme/linux/1.png",O_RDONLY);if(-1==srcfd){perror("open srcfd");exit(1);}while(1){char buf[4096] = {0};int ret = read(srcfd,buf,sizeof(buf));if(ret<= 0){break;}write(fd,buf,ret);}close(fd);close(srcfd);//system("pause");return 0;
}
读操作:
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
int main(int argc, char **argv)
{int ret = mkfifo("myfifo", 0666);if (-1 == ret){//如果错误的原因是 不是fifo 文件已经存在,if (EEXIST != errno){perror("mkfifo");exit(1);}}int fd = open("myfifo", O_RDONLY);if (-1 == fd){perror("open");exit(1);}int dstfd = open("2.png", O_WRONLY | O_CREAT | O_TRUNC, 0666);if (-1 == dstfd){perror("open dstfd");exit(1);}while (1){char buf[1024] = {0};int ret = read(fd, buf, sizeof(buf));if(ret<=0){break;}write(dstfd,buf,ret);}close(fd);close(dstfd);// remove("myfifo");// system("pause");return 0;
}
4. 信号通信
进程间通信 → 信号通信 signal
应用:异步通信、中断
信号范围:1~64(32用于应用编程)
4.1 信号发送端
- kill函数
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能:给pid进程发送sig信号
参数:
- pid:接收进程pid
- sig:信号编号(kill -l查看)
返回值:成功0,失败-1
- raise函数
int raise(int sig);
等价于kill(getpid(),int sig)
- alarm函数
unsigned int alarm(unsigned int seconds);
功能:定时发送SIGALRM信号
- pause函数
int pause(void);
功能:进程暂停,直到收到信号
4.2 信号接收处理
4.2.1 信号处理方式
- 默认处理
- 忽略处理(9,19信号不能忽略)
- 自定义处理(9,19信号不能自定义)
4.2.2 信号注册函数
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数:
- handler可以是:
- SIG_DFL:默认处理
- SIG_IGN:忽略处理
- 自定义函数:void fun(int sig)
特殊信号:
- 10 SIGUSR1
- 12 SIGUSR2
(预留给程序员使用的未定义信号)
5. 练习与作业
5.1 基础练习
- 编写信号处理函数,对SIGUSR1和SIGUSR2输出不同语句
- 验证信号反复注册的处理流程(最后注册的有效)
5.2 大作业
-
修改有名管道通信程序,添加信号处理:
- 发送quit时,进程A发送10或12信号
- 进程B收到信号后退出
-
创建多进程程序处理信号:
- 子进程:
- 收到10信号打印a.txt
- 收到12信号打印b.txt
- 收到13信号退出
- 父进程:
- 提示用户输入
- 根据输入决定发送信号编号
- 子进程: