进程间通信

在进程间完成数据传递需要借助操作系统提供的特殊方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。
而现今常用的进程间通信方法有:

  1. 管道(使用最简单)
  2. 信号(开销最小)
  3. 共享映射区(无血缘关系)
  4. 本地套接字(最稳定)

0x01 匿名管道

注:0x01中的管道都指匿名或者无名管道

管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。
调用pipe系统函数即可创建一个管道,有如下特性:

  1. 其本质是一个伪文件(实为内核缓冲区)
  2. 由两个文件描述符引用,一个表示读端,一个表示写端。
  3. 规定数据从管道的写端流入管道,读端流出。

管道的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。

管道的局限性:

  1. 数据不能进程自己写,自己读。
  2. 管道中的数据不能反复读取。一旦读走,管道中不再存在。
  3. 采用半双工通信方式,数据只能在单方向上流动。

pipe函数

pipe函数用来创建并打开无名管道。它首先在系统的文件表中取得两个表项,
然后在当前进程的文件描述符表中也同样寻找两个未使用的描述符表项,用来保存相应的文件结构指针。
接着在系统中申请一个空闲的inode,同时获得管道使用的一个缓冲区。然后对相应的文件结构进行初始化,
将一个文件结构设置为只读模式,另一个设置为只写模式。
最后将两个文件描述符传给用户。

函数原型

1
2
#include <unistd.h>
int pipe(int pipefd[2]);
1
2
3
4
5
6
7
@param:
pipefd[0]: 读端
pipefd[1]:写端

@return:
secessful: 0
failure: -1, set errno

e.g. 父子进程使用管道读写

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>


int main(int argc, char* argv[])
{
int pipefd[2];
char buf[1024] = "";
if((pipe(pipefd)) == -1) {
perror("pipe error");
}
int pid = fork();
if(0 == pid) {
close(pipefd[1]); // close unuse write end
read(pipefd[0], buf, sizeof(buf));
write(STDOUT_FILENO, buf, sizeof(buf));
close(pipefd[0]);
}else if (pid > 0){
char *str = "hey\n";
close(pipefd[0]); // close unuse read end
write(pipefd[1], str, strlen(str));
close(pipefd[1]);
wait(NULL);
}else{
perror("fork error");
}

return 0;
}

管道的读写行为:

读管道

  • 管道中有数据:
    read返回实际读到的字节数。
  • 管道中无数据:
    1. 无写端:read返回0(类似读到文件尾)
    2. 有写端:read阻塞等待(进程从运行态进入阻塞态)

写管道

  • 管道读端全部被关闭,进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
  • 管道读端存在:
    1. 管道已满,write阻塞
    2. 管道未满,write返回写出的字节个数

pipe创建的管道可以有一个读端多个写端,也可以有一个写端多个读端, 但是这里的读写顺序就可能和预期不符,要加上一些特殊操作来实现同步读写。

e.g. 实现一个ls | wc -l的执行结果

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>


int main(int argc, char* argv[])
{
int i = 0;
int fd[2];
if (pipe(fd) == -1) {
perror("pipe error");
exit(-1);
}

for(;i < 2; ++i){
if((fork() == 0)) break;
}

pid_t cpid = 0;
switch(i) {
case 0: // ls
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
perror("execlp ls error");
exit(-1);
break;
case 1: // wc
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
// execlp("ls", "ls", "-l", NULL); //执行ls就不需要父进程关闭写端
perror("execlp wc error");
exit(-1);
break;
default:
// close(fd[0]);
// 由于wc的特性,只要输入中没读到文件尾就会一直读取文件内容,
// 而执行ls的子进程在执行后被父进程回收资源,还剩父进程一个写端,此时若不关闭父进程的写端的话,
// 由于管道特性,执行wc的子进程就会一直阻塞等待父进程写
close(fd[1]);
while((cpid = waitpid(-1, NULL, WNOHANG)) != -1){
if(cpid == 0){
sleep(1);
continue;
}else if (cpid > 0){
printf("catch the child %d\n", cpid);
}else {}
}
break;
}
return 0;
}

查看管道缓冲区大小

  1. 使用系统命令ulimit -a来查看
  2. 使用标库函数fpathconf借助参数_PC_PIPE_BUF来查看。
    1
    2
    #include <unistd.h>
    long fpathconf(int fd, int name);
    1
    2
    3
    @return:
    secessful: 返回管道的大小
    failure: -1, set errno

0x02 命名管道FIFO

FIFO常被称为命名管道,以区分管道(PIPE)。管道(PIPE)只能用于“有血缘关系”的进程间。
但通过FIFO,不相关的进程也能交换数据。
FIFO是LIinux基础文件类型中的一种。但FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。
各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。

创建方式:

  1. 命令:mkfifo 管道名
  2. 标库函数mkfifo
    1
    2
    3
    4
    5
    6
    7
    8
    #include <sys/types.h>
    #include <sys/stat.h>
    /**
    * return:
    secessful: 0
    failure: -1, set errno
    */
    int mkfifo(const char *pathname, mode_t mode);

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

e.g. 使用命名管道进行读写
读端

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>

void say_error(const char *str){
perror(str);
exit(-1);
}

int main(int argc, char* argv[])
{
if(argc != 2){
printf("Usage:\n\t%s fifoname", argv[0]);
return -1;
}
char buf[BUFSIZ];

int fd = open(argv[1], O_RDONLY);
if(fd < 0){
say_error("open error");
}
while(1){
read(fd, buf, BUFSIZ);
write(STDOUT_FILENO, buf, strlen(buf));
}

close(fd);

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>

void say_error(const char *str){
perror(str);
exit(-1);
}

int main(int argc, char* argv[])
{
if(argc != 2){
printf("Usage:\n\t%s fifoname", argv[0]);
return -1;
}
char buf[BUFSIZ];

int fd = open(argv[1], O_WRONLY);
if(fd < 0){
say_error("open error");
}
for(int i = 0; ;++i){
sprintf(buf, "hello %d\n", i);
write(fd, buf, strlen(buf));
sleep(1);
}

close(fd);

return 0;
}

共享内存映射

内存映射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,
int fd, off_t offset);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@param:
addr: 指定映射区的首地址。通常传NULL,让系统自动分配
length:共享内存映射区的大小. (通常小于等于 文件的实际大小)
prot:共享内存映射区的读写属性。
- PROT_EXEC Pages may be executed.
- PROT_READ Pages may be read.
- PROT_WRITE Pages may be written.
- PROT_NONE Pages may not be accessed.
flags: 标注共享内存的共享属性.
- MAP_SHARED 共享
- MAP_PRIVATE 私有
- MAP_ANON 创建匿名映射区 , 这种映射区只能用于有血缘关系的进程
- MAP_ANONYMOUS 创建匿名映射区 , 这两种方式不需要fd,fd可以传入-1
,也可使用open`/dev/zero`的fd, 其文件大小为无穷大
fd: 用于创建共享内存映射区的那个文件的文件描述符.
offset:默认0, 表示映射文件全部。偏移位置。需要是内存页(4K)的整数倍

@return:
成功:映射区的首地址
失败:返回 MAP_FAILED <- ((void *)-1) , set errno

munmap函数

用来释放mmap建立的映射区

函数原型

1
2
3
4
5
6
7
8
9
10
#include <sys/mman.h>
/**
@addr: 映射区的首地址
@length:共享内存映射区的大小

return:
成功:0
失败:-1, set errno
*/
int munmap(void *addr, size_t length);

使用示例

简单地通过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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>

void say_error(const char *str)
{
perror(str);
exit(-1);
}

int main(int argc, char* argv[])
{
char *p = NULL;
int fd;
fd = open("testmap", O_RDWR | O_CREAT | O_TRUNC, 0644);
if(fd == -1) {
say_error("open error");
}

ftruncate(fd, 30);
int len = lseek(fd, 0, SEEK_END);

p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED) say_error("mmap error");

// 使用p对文件进行读写操作
strcpy(p, "hello world\n"); //写操作,现在可以相当于操作char型数组一样操作文件了

printf("-----%s\n", p);
if((munmap(p, len)) == -1) {
say_error("munmap error");
}

close(fd);
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
40
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>


void say_error(const char *str)
{
perror(str);
exit(-1);
}

int main(int argc, char* argv[])
{
int fd;
fd = open("testmap", O_RDWR | O_CREAT | O_TRUNC, 0644);
if(fd < 0) say_error("open error");
ftruncate(fd, sizeof(int));

int len = lseek(fd, 0, SEEK_END);
int *p = (int *)mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED) say_error("mmap error");
close(fd);
*p = 200;
printf("start p=%d\n", *p);

if(fork() == 0){ //write
*p = 100;
printf("I'm child p=%d\n", *p);
}else{ //read
sleep(1);
printf("I'm parent p=%d\n", *p);
}


if(munmap(p, len) == -1) say_error("munmap error");
return 0;
}

运行结果

1
2
3
start p=200
I'm child p=100
I'm parent p=100

父子进程使用匿名映射区通信

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>


void say_error(const char *str)
{
perror(str);
exit(-1);
}

int main(int argc, char* argv[])
{
int fd;
fd = open("/dev/zero", O_RDWR);
if(fd < 0) say_error("open error");

int len = sizeof(int);

int *p = (int *)mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, fd, 0);
// int *p = (int *)mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if(p == MAP_FAILED) say_error("mmap error");
close(fd);
*p = 200;
printf("start p=%d\n", *p);

if(fork() == 0){
*p = 100;
printf("I'm child p=%d\n", *p);
}else{
sleep(1);
printf("I'm parent p=%d\n", *p);
}


if(munmap(p, len) == -1) say_error("munmap error");
return 0;
}

运行结果:

1
2
3
start p=200
I'm child p=100
I'm parent p=100

无血缘关系进程间通信

两个进程,一个写,一个读,打开同一个文件创建映射区且flagsMAP_SHARED

  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
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/mman.h>

    typedef struct _Student{
    int id;
    char name[256];
    }Student;


    void say_error(const char *str)
    {
    perror(str);
    exit(-1);
    }

    int main(int argc, char* argv[])
    {
    int fd;
    fd = open("testmap", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if(fd < 0) say_error("open error");
    ftruncate(fd, sizeof(Student));

    int len = lseek(fd, 0, SEEK_END);
    Student *p = (Student *)mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED) say_error("mmap error");
    close(fd);
    p->id = 0;
    while(1){
    sprintf(p->name, "wakaka_%d", ++p->id);
    sleep(2);
    }

    printf("write over....\n");

    if(munmap(p, len) == -1) say_error("munmap error");
    return 0;
    }
  2. 读端

    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 <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/mman.h>

    typedef struct _Student{
    int id;
    char name[256];
    }Student;


    void say_error(const char *str)
    {
    perror(str);
    exit(-1);
    }

    int main(int argc, char* argv[])
    {
    int fd;
    fd = open("testmap", O_RDONLY);
    if(fd < 0) say_error("open error");
    ftruncate(fd, sizeof(Student));

    int len = lseek(fd, 0, SEEK_END);
    Student *p = (Student *)mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
    if(p == MAP_FAILED) say_error("mmap error");
    close(fd);
    while(1){
    printf("Student id:%d\nStudent name:%s\n", p->id, p->name);
    sleep(1);
    }

    printf("read over....\n");

    if(munmap(p, len) == -1) say_error("munmap error");
    return 0;
    }

使用注意事项

  1. 用于创建映射区的文件大小为0, 实际指定非0大小创建映射区,出“总线错误”, 故用于映射的文件必须要有实际大小。
  2. 用于创建映射区的文件大小为0, 实际指定0大小创建映射区,出“无效参数”
  3. 用于创建映射区的文件属性为只读, 映射区属性为读、写,出“无效参数”
  4. 创建映射区,需要read权限。
    • 当访问权限为MAP_SHARED时:mmap的读写权限 应该 <= 文件的open权限. 不能只写
    • 当访问权限为MAP_PRIVATE时:open文件的权限只需要读权限,用于创建映射区即可
  5. 文件描述符fd,在mmap创建映射区完毕后即可关闭。后续访问文件,用地址(指针)访问。
  6. offset 必须是4K的整数倍(MMU 映射的最小单位4K)
  7. 对申请的映射区内存,不能越界访问。
  8. munmap函数用于释放的地址必须是mmap申请返回的地址。
  9. 映射区访问权限为MAP_PRIVATE时,对内存的所有修改,只在内存中有效,不会影响到物理磁盘上的文件。
  10. 在非血缘关系的进程间使用匿名映射区无法达到进程间通信的效果,/dev/zero也不行。