进程间通信笔记(一)

进程间通信

  • pipe进行父子进程间的通信
  • pipe进行兄弟进程间的通信
  • FIFO进行无血缘关系的进程间通信(FIFO又被称为命名管道)
  • mmap函数进行有血缘无血缘进程间通信

IPC

  • InerProcess Communication(进程间通信)

进程间通信的4种方式

  • 管道 - 简单
  • 信号 - 系统开销小
  • 共享映射区 - 有无血缘关系的进程间通信都可以
  • 本地套接字 - 稳定

匿名管道pipe

这里指的是匿名管道pipe

匿名管道概念

  • 本质
    • 内核缓冲区
    • 伪文件,匿名管道在硬盘上没有这个文件,也不占用磁盘空间
  • 特点
    • 两部分
      • 读端,写端,对应两个文件描述符
      • 数据写端流入,读端流出
    • 操作管道的进程被销毁之后,管道自动被释放了
    • 管道默认是阻塞的
      • 读写

匿名管道的原理

  • 内部实现方式:队列

    • 环形队列
    • 特点:先进先出,数据结构特点:不用频繁移动内部元素,队头队尾会随元素移出相对移动。
  • 缓冲区大小

    • 默认4k
    • 大小会根据实际情况做适当调整

匿名管道的局限性

  • 队列
    • 数据只能读取一次,不能重复读取
  • 半双工
    • 单工:遥控器
    • 半双工:对讲机
      • 数据传输方向是单向的
    • 双工:电话
  • 匿名管道pipe
    • 适用于有血缘关系的进程

创建匿名管道

  • int pipe(int fd[2]);
    • fd 为传出参数
    • fd[0]为读端
    • fd[1]为写端

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int main()
{
int fd[2];
int ret = pipe(fd);
if(ret == -1)
{
perror("piper error");
exit(1);
}
pid_t pid = fork();//fork在管道之后,这样子进程能继承父进程的文件描述符
if(pid == -1)
{
perror("fork error");
exit(1);
}
printf("pipe[0] = %d\n" fd[0]);
printf("pipe[1] = %d\n" fd[1]);
close(fd[0]);
close(fd[1]);
return 0;
}

父子进程使用匿名管道通信

  • 练习

    • 父子进程间通信,实现 ps aux | grep bash

      • 数据重定向:dup2(oldfd,newfd)
      • execlp
      • 因为管道的数据只能读一次,为了防止父进程把数据写了自己读了,而且有时候会出现浪费系统资源的现象
        • 父进程写:关闭读端
        • 子进程读:关闭写端
        • 如果需要双向通信 :那就要用两个管道了
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      int main()
      {
      int fd[2];
      int ret = pipe(fd);
      if(ret == -1)
      {
      perror("piper error");
      exit(1);
      }
      pid_t pid = fork();//fork在管道之后,这样子进程能继承父进程的文件描述符
      if(pid == -1)
      {
      perror("fork error");
      exit(1);
      }
      //父进程
      // ps aux
      if(pid>0)
      {
      //关闭读端
      close(fd[0]);
      //文件描述符重定向
      dup2(fd[1],STDOUT_FILENO);
      execlp("ps","ps","aux",NULL);
      perror("execlp");
      exit(1);
      }
      else if(pid == 0)
      {
      close(fd[1]);
      dup2(fd[0],STDIN_FILENO);
      execlp("grep","grep","bash",NULL);
      }
      printf("pipe[0] = %d\n" fd[0]);
      printf("pipe[1] = %d\n" fd[1]);
      close(fd[0]);
      close(fd[1]);
      return 0;
      }
  • 兄弟进程间通信,实现 ps aux |grep bash

    • 把父进程的读和写都close掉,父进程只负责回收子进程的pcb

匿名管道的读写行为

  • 读操作

    • 有数据
      • read(fd) :正常读,返回读出的字节数
    • 无数据
      • 写端全部关闭
        • read解除阻塞,返回0
        • 相对于读文件读到了尾部
      • 没有全部关闭
        • read阻塞
  • 写操作

    • 读端全部关闭
      • 管道破裂,进程被终止
        • 内核给当前进程发信号SIGPIPE
    • 读端没全部关闭
      • 缓冲区写满了
        • write阻塞
      • 缓冲区没有满
        • write继续写
  • 如何设置非阻塞?

    • 默认读写两端都阻塞

    • 设置读端为非阻塞pipe(fd)

      • fcntl - 变参函数

        • 修改文件属性:open的时候对应的flag属性
      • 设置方法:

        • 获取原来的flag:int flag = fcntl(fd[0],F_GETFL);
        • 生成新的flag:flag |= NONBLOCK;
        • 设置:fcntl(fd[0],F_SETFL,flags);

有名管道FIFO

特点

  • 有名管道
  • 在磁盘上有个文件,如果 ls -l 文件类型是 p
  • 伪文件,在磁盘上大小永远为0,往里面写的数据,会被映射到内核缓冲区
  • 在内核中有一个对应的缓冲区
  • 半双工

使用场景

  • 可以进行没有血缘关系的进程间通信

创建方式

  • 命令:mkfifo 管道名
  • 函数:mkfifo

FIFO文件可以使用IO函数进行操作

  • open/close
  • read/write
  • 不能执行lseek操作

进程间通信

  • 两个不相干的进程

使用

就像对待文件一样,创建,两个程序读写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define PATHNAME "/tmp/myfifo"
int main (void)
{
pid_t pid;
int fd = -1;
char buf[BUFSIZ] = "";
// 创建一个命名管道,大家可以用 ls -l 命令查看这个管道文件的属性
if (mkfifo(PATHNAME, 0644) < 0) {
perror("mkfifo()");
exit(1);
}
fflush(NULL);
pid = fork();
if (pid < 0) {
perror("fork()");
exit(1);
}
if (!pid) { // parent
pid = fork();
if (pid < 0) {
perror("fork()");
exit(1);
}
if (!pid) { // parent
// 两个子进程都创建完之后父进程直接退出,使两个子进程不具有亲缘关系。
exit(0);
}
/* child 2 */
/* 像操作普通文件一样对这个管道进行 open(2)、read(2)、write(2)、close(2) */
fd = open(PATHNAME, O_RDWR);
if (fd < 0) {
perror("open()");
exit(1);
}
read(fd, buf, BUFSIZ);
printf("%s", buf);
write(fd, " World!", 8);
close(fd);
exit(0);
} else { // child 1
fd = open(PATHNAME, O_RDWR);
if (fd < 0) {
perror("open()");
exit(1);
}
write(fd, "Hello", 6);
sleep(1); // 刚写完管道不要马上读,等第二个进程读取完并且写入新数据之后再读。
read(fd, buf, BUFSIZ);
close(fd);
puts(buf);
// 肯定是这个进程最后退出,所以把管道文件删除,不然下次再创建的时候会报文件已存在的错误
remove(PATHNAME);
exit(0);
}
return 0;
}

内存映射区

mmap - 创建内存映射区

1
2
3
4
5
6
7
8
#include <sys/mman.h>
void * mmap (void *addr,
size_t len,
int prot,
int flags,
int fd,
off_t offset);
  • addr
    这个参数是建议地址(hint),没有特别需求一般设为NULL。这个函数会返回一个实际 map 的地址。

  • len
    文件长度,不能为0

  • prot
    表明对这块内存的保护方式,不可与文件访问方式冲突。

    • PROT_READ
      • 读权限,必须要有的权限
    • PROT_WRITE
      • 写权限
    • PROT_EXEC
      • 执行权限

    使用 | 进行连接

  • flags

    • MAP_PRIVATE
      • 修改了内存数据不会同步到磁盘
    • MAP_SHARED
      • 修改内存数据会同步到磁盘。
      • MAP_PRIVATE冲突。
  • fd
    文件描述符。进行 map 之后,文件的引用计数会增加。因此,我们可以在 map 结束后关闭 fd,进程仍然可以访问它。当我们 munmap 或者结束进程,引用计数会减少。

  • offset

    • 文件偏移,从文件起始算起。必须是4K的整数倍
    • 或者为 0
  • 返回值

    如果成功,返回指向映射区首地址的指针。

    如果失败,mmap 函数将返回 MAP_FAILED

munmap 释放内存映射区

  • 函数原型: int munmap(void *addr,size_t length);
    • addr :mmap的返回值,映射区的首地址
    • length:mmap的第二个参数,映射区的长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdlib.h>
#include<sys.mman.h>
#include<fcntl.h>
int main(){
int fd = open("english.txt",O_RDWR);
int len = lseek(fd,0,SEEK_END);
//创建内存映射区
void *ptr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED, fd ,0);
if(ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
printf("%s",(char*)ptr);
//释放内存映射区
munmap(ptr,len);
close(fd);
return 0;
}

使用mmap中的一些注意事项

  • 如果对mmap的返回值做操作是不可以的。
    • 因为munmap需要这个值进行释放操作,所以不能动。
    • 需要新建一个指针等于这个指向地址,在对新建指针进行操作。char *pt = ptr;
  • open 文件的权限要大于等于 mmap创建内存映射区的权限,而且必须要有读权限。
  • 可以在open的时候O_CREAT一个新文件来创建映射区么?
    • 可以,但只创建没有大小不可以,需要做文件拓展
      • lseek
      • 或truncate(path,length);
  • mmap后关闭文件描述符,对mmap映射有没有影响?
    • 没有
  • 对ptr越界操作会怎么样?
    • 段错误

mmap 优势

比文件IO操作要更快,是直接在内存上处理的,不是在磁盘上,效率高,

但mmap不阻塞,用的时候要考虑读写先后顺序。

mmap 创建匿名映射区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include<stdlib.h>
#include<sys.mman.h>
#include<fcntl.h>
int main(){
int len = 4096;
//创建匿名映射区
void *ptr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED|MAP_ANON, -1 ,0);
if(ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
//创建子进程
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
if(pid > 0)
{ //写数据
strcpy((char*)ptr,"复制进去一句话");
//回收子进程
wait(NULL);
}
else if(pid == 0)
{
// 读数据
printf("%s\n",(char*)ptr);
}
printf("%s",(char*)ptr);
//释放内存映射区
munmap(ptr,len);
return 0;
}

对于有血缘关系的进程,用mmap创建匿名文件映射区,不用管磁盘上到底有没有这个文件,更方便。

mmap对于没有血缘关系进程通信

对于没有血缘关系的进程,需要借助磁盘文件创建映射区

  • 不阻塞
  • a.c
    • 打开文件用mmap对映射区进行读写操作
  • b.c
    • 打开文件用mmapd对映射区做读写操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// a.c
#include<stdlib.h>
#include<sys.mman.h>
#include<fcntl.h>
int main(){
//打开磁盘文件,没有就创建
int fd = open("temp", O_RDWR|O_CREAT,0664);
//给创建的文件做一个扩展
ftruncate(fd,4096);
//求文件大小,虽然扩展自己知道4096,但是万一改变了还需要改
int len = lseek(fd,0,SEEK_END);
//创建mmap
void* ptr = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(1);
}
while(1)
{
sleep(1);
//读取映射区的时候不必从头读
printf("%s\n",(char*)ptr+1024);
}
//释放
int ret = munmap(ptr,len);
if(ret == -1)
{
perror("munmap");
exit(1);
}
return 0;
}

另一个程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// b.c
#include<stdlib.h>
#include<sys.mman.h>
#include<fcntl.h>
int main(){
//打开同一个磁盘文件
int fd = open("temp", O_RDWR|O_CREAT,0664);
//求文件大小
int len = lseek(fd,0,SEEK_END);
//创建mmap
void* ptr = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(1);
}
while(1)
{
//写数据,写的方式就是对指针进行操作即可
char* p = (char*)ptr;
//和读对应,也偏移1024
p+=1024;
strcpy(p,"hello,world\n");
//特意写慢一些,那边读是不阻塞的,所以每2秒会有一次输出空行
sleep(2);
}
//释放
int ret = munmap(ptr,len);
if(ret == -1)
{
perror("munmap");
exit(1);
}
return 0;
}

补充:dup和dup2

复制文件描述符

dup

  • int dup(int oldfd);
    • oldfd:要复制的文件描述符
    • 返回值:新的文件描述符
    • dup调用成功
      • 有两个文件描述符指向同一个文件
    • 返回值:取最小的且没被暂用的文件描述符
    • 复制完有两个文件描述符,close掉其中一个,文件没有被关闭,还有一个文件描述符。
  • int dup2(int oldfd, int newfd);
    • oldfd ->hello
    • newfd ->world
    • 假设newfd已经指向了一个文件,首先断开close与那个文件的链接,newfd指向oldfd指向的文件

本文标题:进程间通信笔记(一)

文章作者:Yang Shuai

发布时间:2019年03月10日 - 15:03

最后更新:2019年03月10日 - 15:03

原始链接:https://ysbbswork.github.io/2019/03/10/进程间通信笔记(一)/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!