一、进程间通信常用的方式
(1) 管道
(2) System V
(3) POSIX
二、目的
(1) 数据传输
一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
(2) 资源共享
多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
(3) 通知事件
一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
(4) 进程控制
有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
三、 本质
让两个不同的进程看到一份公共的资源
四、匿名管道
1. 局限性
半双工,数据只能在一个方向上移动
只能在具有公共祖先的进程之间使用
2. 简单用法:
3. 创建管道
(1)pipe 函数
#include <unistd.h>
int pipe(int pipefd[2]);
// 成功:0 失败:-1
注意:pipefd 是输出型参数,它会返回两个参数,pipefd[0] 是读,pipefd[1] 是写
(2)代码说明
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
int fd[2];
int ret = pipe(fd);
if (ret < 0)
{
perror("pipe");
return 1;
}
pid_t id = fork();
// fd[0] 读,fd[1] 写
printf("fd[0], fd[1] -> %d, %d\n", fd[0], fd[1]);
if (id < 0)
{
printf("fork error!\n");
}
else if (id == 0)
{
// child
close(fd[0]);
const char* msg = "I am child !";
for (int i = 0; i < 5; i++)
{
write(fd[1], msg, strlen(msg));
sleep(1);
}
}
else
{
// parent
close(fd[1]);
char buf[1024];
while (1)
{
ssize_t s = read(fd[0], buf, sizeof(buf) - 1);
if (s > 0)
{
buf[s] = 0;
printf("father get a message: %s\n", buf);
}
}
}
return 0;
}
父进程创建管道,父进程 fork 子进程,父进程关闭 fd[1](写),子进程关闭 fd[0](读)
(3)结果
4. 特性
(1) 单向传递
(2) 具有血缘关系(父子,兄弟,爷孙)的进程之间,常用于父子之间
(3) 自带同步互斥4
(4) 生命周期随进程(进程退出,管道释放)
(5) 管道在进行通信的时候,对外层提供面向字节流的服务,以字节来读取和写入,字节数的大小完全取决于自己
5. 几个概念
(1) 数据不一致:读写双方因为访问共同资源二导致的数据不一致的问题
例如: hello word 和 shagua
读第一个读到空格时,读到第二个,就出现了错误
(2) 临界资源:把两个进现在程看到的看到的公共资源称为临界资源
(3) 临界区:每个进程中访问临界资源的那段代码称为临界区
(4) 互斥:在任何一个时间点,临界区在访问临界资源时,有且仅有一个人在访问,即你用的时候别人都不能用,别人用的时候,你也不能去用
同步:在现有的互斥基础上,让多个人访问临界资源具有了顺序性,即我们大家利用一些共同的资源区,大家一起合作,完成某些事情,但是我在干某些小事的时候,可能要等到你做完另一些小事
(5) 原子性:在进行某些资源的操作时时,不会有中间状态
6. 读写规则
(1) 写方一直在写,读端不读,但读端不关闭文件描述符,写方因为互斥与同步在把管道写满之后就会停下来
(2) 如果读写双方不挂不关闭文件描述符,一个不写,一个不读,双方都要等待,这个过程就叫同步
(3) 如果写端一直在写,有朝一日写端不写,并把写端关闭,读端会一直读,直到把管道中文件描述符读完,最后读返回值 0,这个 0 代表着读到了文件的结尾
(4) 写端一直在写,读端不读且关闭文件描述符,写端会因为操作异常被操作系统向目标发送13号信号(SIGPIPE)终止
7. 管道大小
管道大小为:512*8 = 4096
7. popen 与 pclose+
#include <stdio.h>
FILE *popen(const char *command, const char *type);
// 成功:文件指针 失败:NULL
int pclose(FILE *stream);
// 成功:command 的终止状态 失败:-1
popen:先执行 fork,然后 exec command,返回一个标准 I/O 的文件指针
pclose:关闭标准 I/O 流,返回 shell 终止状态
五、命名管道
1. 创建命名管道
(1)函数创建
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int mkfifo(const char *pathname, mode_t mode);
int mkfifoat(int dirfd, const char *pathname, mode_t mode);
// 成功:0 失败:-1
(1)如果 pathname 是绝对路径,fd 被忽略,与 mkffio 类似
(2)如果 pathname 是相对路径,fd 是一个打开目录的有效文件描述符,路径名和目录有关
(3)如果 pathname 是相对路径,fd 有一个特殊值 AT_FDCWD,路径名以当前目录开始,与 mkffio 类似
(2)命令行创建
mkfifo filename
2. 读写规则
如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
3. 用途
(1)shell 命令使用 FIFO 将数据从一条管道传送到另一条时,无需创建临时文件
(2)客户进程 - 服务进程,FIFO用作汇聚点,在客户进程服务进程之间传递数据
代码说明:
reader.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("./myfifo", O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
printf("open ok!\n");
while (1)
{
char buf[1024] = {0};
ssize_t read_size = read(fd, buf, sizeof(buf) - 1);
if (read_size < 0)
{
perror("read");
return 1;
}
if (read_size == 0)
{
perror("read done");
return 0;
}
buf[read_size] = '\0';
printf("%s\n", buf);
}
close(fd);
return 0;
}
writer.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("./myfifo", O_WRONLY);
if (fd < 0)
{
perror("open");
return 1;
}
while (1)
{
printf("> ");
fflush(stdout);
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0)
{
perror("read");
return 1;
}
if (read_size == 0)
{
printf("read done!\n");
return 0;
}
buf[read_size] = '\0';
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}
运行截图: