目录
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信号退出
- 父进程:
- 提示用户输入
- 根据输入决定发送信号编号
- 子进程: