0%

unix环境高级编程 - 进程间通信

最近在学习 APUE,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习APUE的童鞋们提供一份参考。

本系列博文均根据学习《UNIX环境高级编程》一书总结而来;

运行环境:

  • 操作系统: ubutnu 16.04
  • 编译器:QtCreator CLion 2020.3

进程间通信

进程间通信的基本大概可以理解为:两个进程要想完成数据交换,必须通过内核,一个进程将输入写入内核,另一个进程从内核读走数据;

pipe

管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道。

  • 管道的本质是一块内核缓冲区

  • 由两个文件描述符引用,一个表示读端,一个表示写端。

  • 规定数据从管道的写端流入管道,从读端流出。

  • 当两个进程都终结的时候,管道也自动消失。

  • 管道的读端和写端默认都是阻塞的。

管道原理

  • 管道的实质是内核缓冲区,内部使用环形队列实现。

  • 默认缓冲区大小为4K,可以使用ulimit -a命令获取大小。

  • 实际操作过程中缓冲区会根据数据压力做适当调整。

  • 数据一旦被读走,便不在管道中存在,不可重复读数据

  • 数据只能在一个方向流动,若要实现双向管道,必须使用两个管道

  • 只能在有学院关系的进程间使用

管道使用

一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在血缘关系,这里的血缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。父子进程间具有相同的文件描述符,且指向同一个管道**pipe**,其他没有关系的进程不能获得pipe()产生的两个文件描述符,也就不能利用同一个管道进行通信。

  • 父进程创建管道

  • 父进程fork出子进程

  • 父进程关闭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
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
/**
* @file 管道函数
*
* 示例程序 - 01_pipe.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//1.创建管道
//int pipe(int pipefd[2]);
int fd[2];
int ret = pipe(fd);
if(ret < 0)
{
perror("pipe error");
return -1;
}
//2.创建子进程
pid_t pid = fork();
if (pid < 0)
{
perror("fork error");
return -1;
}

else if(pid > 0) //父进程写
{
//关闭读段
close(fd[0]);
sleep(5);
write(fd[1],"sYstemk1t",sizeof("sYstemk1t"));
wait(NULL);
}
else //子进程读
{
//关闭写
close(fd[1]);
char buf[64];
memset(buf,0,sizeof(buf));
int nCount = read(fd[0],buf,sizeof(buf));
printf("nCount = %d buf = %s\n",nCount,buf);
}
return 0;
}

Test:

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
/**
* @file 管道使用ps aux |grep bash
*
* 示例程序 - 02_pipe_parent.c
*
* @author Steve & sYstemk1t
*
*/

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//1.创建管道
//int pipe(int pipefd[2]);
int fd[2];
int ret = pipe(fd);
if(ret < 0)
{
perror("pipe error");
return -1;
}
//2.创建子进程
pid_t pid = fork();
if (pid < 0)
{
perror("fork error");
return -1;
}

else if(pid > 0) //父进程写
{
//关闭读段
close(fd[0]);
//将标准输出重定向到管道写段
dup2(fd[1],STDOUT_FILENO);
execlp("ps","ps","aux",NULL);

perror("execlp error");
wait(NULL);
}
else //子进程读
{
//关闭写
close(fd[1]);
//将标注输入重定向到管道读端
dup2(fd[0],STDIN_FILENO);

execlp("grep","gerp","--color=auto","bash",NULL);
perror("execlp error");
}
return 0;
}

管道读写行为

  • 读操作

    • 有数据

      • read正常读,返回读出的字节数
    • 无数据

      • 写端全部关闭

        • read解除阻塞,返回0, 相当于读文件读到了尾部
      • 没有全部关闭

        • read阻塞
  • 写操作

    • 读端全部关闭

      • 管道破裂,进程终止, 内核给当前进程发SIGPIPE信号
    • 读端没全部关闭

      • 缓冲区写满了

        • write阻塞
      • 缓冲区没有满

        • 继续write

管道阻塞

默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参

考下列三个步骤进行:

1
2
3
4
5
1步: int flags = fcntl(fd[0], F_GETFL, 0); 

2步: flag |= O_NONBLOCK;

3步: fcntl(fd[0], F_SETFL, flags);

若是读端设置为非阻塞:

  • 写端没有关闭,管道中没有数据可读,则read返回-1;

  • 写端没有关闭,管道中有数据可读,则read返回实际读到的字节数

  • 写端已经关闭,管道中有数据可读,则read返回实际读到的字节数

  • 写端已经关闭,管道中没有数据可读,则read返回0

查看管道缓冲区大小

1
2
3
ulimit -a
printf("pipe size == [%ld]\n",fpathconf(fd[0],_PC_PIPE_BUF));
printf("pipe size == [%ld]\n",fpathconf(fd[0],_PC_PIPE_BUF));

FIFO

FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间通信。但通过FIFO,不相关的进程也能交换数据。

​ FIFO是Linux基础文件类型中的一种(文件类型为p,可通过ls -l查看文件类型)。但FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来标识内核中一条通道。进程可以打开这个文件进行read/write,实际上是在读写内核缓冲区,这样就实现了进程间通信。

创建FIFO

  • 方式1-使用命令 mkfifo

    ​ 命令格式: mkfifo 管道名

    ​ 例如:mkfifo myfifo

  • 方式2-使用函数

1
int mkfifo(const char *pathname, mode_t mode);

参数说明和返回值可以查看man 3 mkfifo

当创建了一个FIFO,就可以使用open函数打开它,常见的文件I/O函数都可用于FIFO。如:close、read、write、unlink等。

FIFO严格遵循先进先出(first in first out),对FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek**()**等文件定位操作。

使用FIFO完成两个进程通信

使用FIFO完成两个进程通信的示意图

进程A:

  • 创建一个FIFO文件:mkfifo命令或使用mkfifo函数
  • open file文件,获得一个文件描述符fd
  • 写fifo文件——write
  • 关闭fifo文件—-close

进程B:

  • 打开fifo文件,获得文件描述符fd
  • 读fifo文件——read
  • 关闭fifo文件—-clsoe

access

1
2
3
4
5
6
7
8
9
10
11
int access(const char* pathname, int mode);

F_OK 值为0,判断文件是否存在

X_OK 值为1,判断对文件是可执行权限

W_OK 值为2,判断对文件是否有写权限

R_OK 值为4,判断对文件是否有读权限

注:后三种可以使用或“|”的方式,一起使用,如W_OK|R_OK

内存映射区

简介

存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。

使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。

mmap

1
2
3
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags,
  • addr: 指定映射的起始地址, 通常设为NULL, 由系统指定

  • length:映射到内存的文件长度

  • prot: 映射区的保护方式, 最常用的:

    ​ 读:PROT_READ

    ​ 写:PROT_WRITE

    ​ 读写:PROT_READ | PROT_WRITE

  • flags: 映射区的特性, 可以是

    ​ MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享。

    ​ MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。

  • fd:由open返回的文件描述符, 代表要映射的文件。

  • offset:以文件开始处的偏移量, 必须是**4k**的整数倍, 通常为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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* @file mmap匿名映射
*
* 示例程序 - 09_mmap_anony.c
*
* @author Steve & sYstemk1t
*
*/

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

int main()
{
//1.建立内存映射区
int fd = open("test.log",O_RDWR | O_CREAT,0777);
if(fd < 0)
{
perror("open error");
return -1;
}
int len = lseek(fd,0,SEEK_END);

//void *addr = mmap(NULL,len,PROT_READ | PROT_WRITE, MAP_SHARED,fd,0);
void *addr = mmap(NULL,4096,PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS,-1,0); //文件描述符必须为-1
if(addr == MAP_FAILED)
{
perror("mmap error");
return -1;
}



//2.创建子进程
pid_t pid = fork();
if(pid < 0)
{
perror("fork error");
return -1;
}
else if(pid > 0) //父进程
{
memcpy(addr,"Hello,sYstemk1t",strlen("Hello,sYstemk1t"));
wait(NULL);
}
else //子进程
{
sleep(1);
char *p = (char *)addr;
printf("addr = %s\n",p);
}

return 0;
}

munmap

1
int munmap(void *addr, size_t length);

注意

  • 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区

  • 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。

  • 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。

  • 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。

  • munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。

  • 文件偏移量必须为0或者4K的整数倍

  • mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。