一、进程间通信IPC(InterProcess Communication)
进程间通信的常用方式,特征:
1. 管道:简单
2. 信号:开销小
3. 共享存储映射(mmap)映射:非血缘关系进程间
4. socket(本地套接字):最稳定
二、管道
真文件:普通文件、目录文件、软链接--->需要占用磁盘空间
伪文件:字符设备文件、块设备文件、管道文件、套接字--->不占用磁盘空间,只占用内存(缓冲区)
实现原理: 内核借助环形队列机制,使用内核缓冲区实现。
特质:
1. 伪文件
2. 管道中的数据只能一次读取。
3. 数据在管道中,只能单向流动。
局限性:
1. 自己写,不能自己读。
2. 数据不可以反复读。
3. 半双工通信。
4. 血缘关系进程间可用
管道的基本用法:
int pipe(int fd[2]);
创建,并打开管道。
参数:
fd[0]: 读端。
fd[1]: 写端。
返回值:
成功: 0
失败: -1 errno
管道通信原理:
1.父进程调用了pipe(),相当于创建了一个管道并打开了读端和写端,pipefd[0]是读端,pipefd[1]是写端
2.父进程fork出一个子进程,此时子进程也掌握着父进程管道的读端和写端
3.父进程写入数据(读端关闭),子进程读取数据(写端关闭)
pipe demo:父进程往管道里写,子进程从管道读,然后打印读取的内容到屏幕上
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int ret;
int fd[2];
pid_t pid;
char *str = "hello pipe\n";
char buf[1024];
ret = pipe(fd);
if (ret == -1){
sys_err("pipe error");}
pid = fork();
if (pid > 0){
close(fd[0]); //父进程关闭读端
write(fd[1],str,strlen(str));//写入数据
sleep(1); //让终端提示和输出不混杂在一块
close(fd[1]);//写后关闭写端
}else if(pid == 0){
close(fd[1]); //子进程关闭写端
ret = read(fd[0],buf,sizeof(buf));//读取数据到buf中
write(STDOUT_FILENO,buf,ret);//将buf的数据打印到屏幕上,ret是字节数
close(fd[0]);//关闭读端
}
return 0;
}
要是不想让终端提示和输出混杂在一起,就在父进程写入内容之后sleep一秒钟
管道的读写行为:
读管道:
1. 管道有数据,read返回实际读到的字节数。
2. 管道无数据:
1)无写端,read返回0 (类似读到文件尾)
2)有写端,read阻塞等待。
写管道:
1. 无读端(读端全关闭), 进程异常终止(SIGPIPE导致的)
2. 有读端:
1) 管道已满, 阻塞等待
2) 管道未满, 返回写出的字节个数。
练习:使用管道实现父子进程间通信,完成:ls | wc –l。假定父进程实现ls,子进程实现wc
要求:(要用到 pipe dup2 exec)
1. 假定父进程实现wc,子进程实现ls
2. ls命令正常会将结果集写到stdout,但现在会写入管道写端
3. wc -l命令正常应该从stdin读取数据,但此时会从管道的读端读。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int fd[2];
int ret;
pid_t pid;
ret = pipe(fd); //父进程先创建一个管道,持有管道的读端和写端
if (ret == -1){
sys_err("pipe error");
}
pid = fork(); //子进程同样持有管道的读和写端
if (pid == -1){
sys_err("fork error");
}else if (pid > 0){ // 父进程读,关闭写端
close(fd[1]);
dup2(fd[0],STDIN_FILENO); //重定向 stdin到管道的 读端
execlp("wc","wc","-l",NULL); // 执行 wc -l 程序
sys_err("execlp wc error");
}else if(pid == 0){
close(fd[0]);
dup2(fd[1],STDOUT_FILENO); //重定向stdout到管道写端
execl("ls","ls",NULL); //子进程执行ls命令
sys_err("execlp ls error");
}
return 0;
}
兄弟间进程通信:
使用管道实现兄弟进程间通信。 兄:ls 弟: wc -l 父:等待回收子进程。
要求,使用“循环创建N个子进程”模型创建兄弟进程,使用循环因子i标示。注意管道读写行为
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int fd[2];
int ret,i;
pid_t pid;
ret = pipe(fd);
if (ret == -1){
sys_err("pipe error");
}
for (i = 0;i < 2;i++) // 表达式2 出口,仅限父进程使用
{
pid = fork();
if (pid == -1 )
{
sys_err("fork error");
}
if (pid == 0) // 子进程,出口
break;
}
if (i == 2) // 父进程 . 不参与管道使用.
{
close(fd[0]);// 关闭管道的 读端/写端
close(fd[1]);
wait(NULL); // 回收子进程
wait(NULL);
}
else if (i == 0)//兄
{
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);// 重定向stdout
execl("ls","ls",NULL);
sys_err("execlp ls error");
}
else if(i == 0)//弟弟
{
close(fd[1]);
dup2(fd[0],STDIN_FILENO);// 重定向 stdin
execlp("wc","wc","-l",NULL);
sys_err("execlp wc error");
}
return 0;
}
注意:父进程不使用管道,所以一定要关闭父进程的管道,保证数据单向流动
允许一个pipe有一个写端多个读端 也允许一个pipe有多个写端一个读端
管道缓冲区大小:
可以使用ulimit –a 命令来查看当前系统中创建管道文件所对应的内核缓冲区大小,通常为:
pipe size (512 bytes, -p) 8
管道的优劣:
优点:简单,相比信号,套接字实现进程间通信,简单很多。
缺点: 1. 只能单向通信,双向通信需建立两个管道。
2. 只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决。
三、FIFO
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。
无血缘关系进程间通信:
读端,open fifo O_RDONLY
写端,open fifo O_WRONLY
创建方式:
1. 命令:mkfifo 管道名
2. 库函数:int mkfifo(const char *pathname, mode_t mode); 成功:0; 失败:-1
利用mkfifo函数创建一个FIFO:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/stat.h>
#include<errno.h>
#include<pthread.h>
int sys_err(const char *str)
{
perror(str);
exit(1);
}
int main(int argc,char *argv[])
{
int ret = mkfifo("mytestfifo",0664);
if(ret == -1)
{
sys_err("mkfifo error");
}
return 0;
}
非血缘关系进程,一个写fifo,一个读fifo,操作起来就像文件一样的:
writefifo.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
int fd,i;
char buf[4096];
if(argc < 2)
{
printf("Enter like this: ./a.out fifoname\n");
return -1;
}
fd = open(argv[1],O_WRONLY);//打开管道文件
if(fd < 0)
{
perror("open");
exit(-1);
}
i = 0;
while(1)
{
sprintf(buf,"hello %d\n",i++);
write(fd,buf,strlen(buf));//向管道写入数据
sleep(1);
}
close(fd);
return 0;
}
readfifo.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc,char *argv[])
{
int fd,i;
char buf[4096];
if(argc < 2)
{
printf("./a.out fifonema\n");
return -1;
}
//int fd = mkfifo("tetsfifo",0664);
//open(fd,...);
fd = open(argv[1],O_RDONLY);//打开管道文件
if(fd<0)
{
perror("open");
exit(-1);
}
while(1)
{
int len = read(fd,buf,sizeof(buf));//从管道的读端读取数据
write(STDOUT_FILENO,buf,len);
sleep(1);//多个读端时应增加睡眠秒数,放大效果
}
close(fd);
return 0;
}