linux系统-进程间通信

进程间通信常见方式

Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷贝到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,Inter Process Communication).

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

  • 管道(使用最简单)
    • pipe
    • 管道的读写行为
    • fifo
      • 用于非血缘关系进程间通信
  • 信号(开销最小),只能携带固定的少量信息
  • 共享映射区
    • mmap
    • 函数的参数使用注意事项
    • 用于非血缘关系的进程通信
  • 本地套接字(最稳定)

管道

基本概念

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

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

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

管道的局限性:

  • 数据不能自己读自己写
  • 数据一旦被读走,便不在管道中存在,不可反复读取
  • 由于管道采用半双工通信方式。因此数据只能在一个方向上流动
  • 只能在有公共祖先的进程间使用管道

管道使用方法

pipe函数: 创建并打开管道。

函数调用成功返回r/w两个文件描述符,无需open,但需手动close。规定:fd[0] :r, fd[1]:w, 类似于0对应标准输入,1对应标准输出向管道文件读写数据其实是在读写内核缓冲区。

1
2
3
4
5
int pipe(int pipefd[2]);

/*pipefd[0]-读端;
pipefd[1]-写端;
成功返回0, 失败返回-1并设置errno;*/

fork完成时父进程关闭读端,子进程关闭写端,此时数据能在pipe中单向流动,父子进程能够完成通信

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

int main(int argc,char* argv[]){
int ret=0;
int pipefd[2];
pid_t pid=0;
char* str="loveyou\n";
char buf[1024];
/*创建管道,文件描述符保存在数组里*/
ret=pipe(pipefd);
if(ret==-1)
perr_exit("pipe error");

pid=fork();
if(pid>0){
close(pipefd[0]); //父进程关闭读端
write(pipefd[1],str,sizeof(str)); //向管道中写入数据
close(pipefd[1]); //父进程关闭写端
}else if(pid==0){
close(pipefd[1]); //子进程关闭写端
ret=read(pipefd[0],buf,sizeof(buf)); //从管道中读取数据
write(STDOUT_FILENO,buf,ret); //写到标准输出打印
close(pipefd[0]);
}
return 0;
}

管道读写行为

  • 读管道
    • 管道中有数据:read返回实际读到的字节数
    • 管道中无数据:
      • 写端全关闭:read返回0
      • 写端没有被全部关闭(仍有写端打开), read阻塞等待(不久的将来可能会有数据抵达, 此时会让出CPU
  • 写管道
    • 管道读端全部被关闭, 进程异常终止(也可以捕捉SIGPIPE信号, 使进程不终止)
    • 有读端打开
      • 管道未满:写数据,write将数据写入,返回写入字节数
      • 管道已满,write阻塞(少见)

获取管道缓冲区大小: ulimit -a

父子进程通信练习:实现 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
//需要使用exec(), dup2(), pipe()
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc,char* argv[]){
int fd[2];
pid_t pid;
int ret=0;

ret=pipe(fd);
if(ret==-1)
perr_exit("pipe error");

pid=fork();
if(pid==-1)
perr_exit("fork error");
if(pid>0){
/*父进程先读管道,如果子进程还没起来,他就会阻塞,这样子进程就会先于父进程结束*/
close(fd[1]);
dup2(fd[0],STDIN_FILENO);
execlp("wc","wc","-l",NULL);
perr_exit("execlp wc error");
}else if(pid==0){
/*子进程写管道*/
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);
execlp("ls","ls",NULL);
perr_exit("execlp ls error");
}
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
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc,char* argv[]){
int fd[2];
pid_t pid;
int ret=0;
int i=0;

/*创建管道*/
ret=pipe(fd);
if(ret==-1)
perr_exit("pipe error");

/*循环创建2个子进程*/
for(i=0;i<2;++i){
pid=fork();
if(pid==-1)
perr_exit("fork error");
if(pid==0)
break;
}

/*父进程关闭管道读写两端*/
if(i==2){
close(fd[0]);
close(fd[1]);
wait(NULL);
wait(NULL);
}else if(i==0){
/*兄进程关闭读端,将STDOUT指向fd[1]*/
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);
execlp("ls","ls",NULL);
perr_exit("execlp ls error");
}else if(i==1){
/*弟进程关闭写端,将STDIN指向fd[0]*/
close(fd[1]);
dup2(fd[0],STDIN_FILENO);
execlp("wc","wc","-l",NULL);
perr_exit("execlp wc error");
}
return 0;
}

注意:管道可以一个读端, 多个写端, 但是不建议这样做。默认管道的大小是4k。

命名管道FIFO

基本概念

为区分pipe,将FIFO称为命名管道。FIFO用于不相关进程间的数据交换

FIFO是Linux基础文件类型中的一种, 但是FIFO文件在磁盘上没有数据块, 仅仅用来标识内核中的一条通道, 各进程可以打开这个文件进行read/write, 实际上是在读写内核通道, 这样就实现了进程间通信

创建方式:

1
2
int mkfifo(const char* pathname,mode_t mode);
//成功返回0, 失败返回-1并设置errno

用FIFO进行通信几乎只有文件读写操作, 比较简单。

文件通信

读普通文件不会造成read阻塞, 如果子进程睡1秒再写, 父进程由于刚开始读不到数据read直接返回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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc,char* argv[]){
int fd=0;
int i=0;
char buf[4096];
/*靠已经创建好的FIFO,如果命令行参数没给指定,报错*/
if(argc<2){
printf("Enter like this:./a.out fifoname\n");
return -1;
}
/*以只写方式打开FIFO文件,拿到fd*/
fd=open(argv[1],O_WRONLY);
if(fd==-1)
perr_exit("open error");

while(1){
/*将数据写到buf中*/
sprintf(buf,"love you:%d\n",i++);
write(fd,buf,strlen(buf));
sleep(1);
}
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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int main(int argc,char* argv[]){
int fd=0;
int i=0;
int len=0;
char buf[4096];
/*同样要依靠已经创建好的FIFO,从命令行参数中指定*/
if(argc<2){
printf("Enter like this:./a.out fifoname\n");
return -1;
}
/*以只读方式打开FIFO文件,拿到fd*/
fd=open(argv[1],O_RDONLY);
if(fd==-1)
perr_exit("open error");
/*从fd中读取数据,并写到标准输出上*/
while(1){
len=read(fd,buf,sizeof(buf));
write(STDOUT_FILENO,buf,len);
sleep(1);
}
close(fd);
return 0;
}

MMAP

基本概念

mmap

存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射, 于是当从缓冲区中取数据, 就相当于读文件中的相应字节。与此类似, 将数据存入缓冲区, 则相应的字节就自动写入文件, 这样就可以在不使用readwrite函数的情况下, 使用指针完成I/O操作。可以借助共享内存和指针来访问磁盘文件。

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

基本使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

/*返回:成功则返回创建的映射区首地址,失败:MAP_FAILED宏((void*)-1), 设置errno;

addr:建立映射区的首地址,由linux内核指定,使用的时候直接传递NULL, 表示让系统自动分配;

length:创建映射区的大小(<=文件的实际大小);

prot: 共享内存映射区的读写属性:PROT_READ, PROT_WRITE, PROT_READ|PROT_WRITE

flags: 标志位参数(常用于设定更新物理区域,设置共享,创建匿名映射区)
MAP_SHARED: 将映射区所做的操作反映到物理设备上(磁盘)
MAP_PRIVATE:映射区所做的修改不会映射到物理设备

fd:用来建立映射区的文件描述符*/
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
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{
int len, ret;
char* p = NULL;
int fd = open("mytest.txt",O_CREAT|O_RDWR,0644);
if(fd < 0)
{
perror("open");
exit(0);
}
len = ftruncate(fd,4); //将文件截断为4byte
if(len == -1)
{
perror("ftruncate");
exit(0);
}
p = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(p == MAP_FAILED)
{
perror("mmap");
exit(0);
}
/*内存的写操作,会被映射到文件的写操作*/
strcpy(p,"abc");//写数据
/*与malloc一样,申请的内存要还回去*/
ret = munmap(p,4);
if(ret == -1)
{
perror("munmap");
exit(0);
}
close(fd);
return 0;
}

od -tcx filename:以16进制查看文件

MMAP使用注意事项

创建映射区的权限应该小于等于文件打开的权限,创建映射区的过程隐含一次对文件的读操作。

段错误:gdb,直接run就可以抓取到段错误的位置。

  • 可以open的时候O_CREAT一个新文件来创建映射区,但是要拓展文件大小, 否则会出现总线错误. 当然 mmap时指定size=0,mmap会报错
  • 如果open时指定O_RDONLYmmapPROT参数指定PROT_READ|PROT_WRITE会报错,无效参数(注意ftruncte()函数需要写权限, 否则无法拓展文件大小). 如果都用只读权限, 不会出错. 要创建映射区, 文件必须有读权限
  • 文件描述符先关闭,对mmap映射没有影响,建立完映射区后fd即可关闭
  • 如果文件偏移量为1000,mmap会报错,因为偏移量必须是4K的整数倍(MMU映射的最小单位为4K)
  • mem进行越界操作:小范围的越界问题不大, 但是最好不要这样(操纵不安全的内存, 操作系统不给保障)
  • 如果mem++,munmap不会成功(与malloc一样, 释放的内存的指针必须是申请得来的初始的指针, 如果要改变指针的值, 拷贝一份用)
  • 除了第一个参数, 后面的参数都可能导致失败
  • 无论mmap多复杂,一定要检查mmap的返回值

总结:

  • 创建映射区过程中,隐含一次对映射文件的读操作
  • MAP_SHARED时,要求映射区的权限应该<=文件打开的权限(出于对映射区的保护),而MAP_PRIVATE则无所谓因为mmap中的权限是对内存的限制
  • 特别注意,当映射区文件大小为0时,不能创建映射区,所以:用于映射的文件必须要有实际大小!!mmap使用时候经常会出现总线错误,通常是由于共享文件存储空间大小引起的
  • munmap传入的地址一定是mmap的返回地址,坚决杜绝指针++操作
  • 文件偏移量必须为4K的整数倍
  • mmap创建映射区出错概率很高,一定要检查返回值,确保映射区建立成功后再进行后续操作

mmap优点

  • 对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

  • 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

  • 提供进程间共享内存及相互通信的方式不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可以直接使用已经保存在内存中的文件数据。

  • 可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>

int var = 100;
int main()
{
int* p;
pid_t pid;

int fd;
fd = open("temp",O_CREAT |O_RDWR|O_TRUNC,0644);
if(fd < 0)
{
perror("open error");
exit(1);
}
unlink("temp"); //删除临时文件目录项,使之具备被释放的条件
ftruncate(fd,4);

p = (int*)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); //共享
// p = (int*)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0); //进程各自独占
if(p == MAP_FAILED) //注意:不是p==NULL
{
perror("mmap error");
exit(1);
}
close(fd); //映射区建立完毕,即可关闭文件
pid = fork();
if(pid == 0) //创建子进程
{
*p = 2000; //给映射区赋值
var = 1000;
printf("child, *p = %d, var = %d\n",*p, var);
} else{
sleep(1);
printf("parent, *p = %d, var = %d\n",*p, var);
wait(NULL);

int ret = munmap(p,4); //释放映射区
if(ret == -1)
{
perror("munmap error");
exit(1);
}
}
}

//child, *p = 2000, var = 1000
//parent, *p = 2000, var = 100

父子等有血缘关系的进程直接也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:

  • MAP_PRIVATE(私有映射): 父子进程各自独占映射区
  • MAP_SHARED(共享映射):父子进程共享映射区

结论:父子进程共享:

  • 打开的文件
  • mmap建立的映射区(但必须使用MAP_SHARED)

匿名映射

使用映射区来完成文件读写操作十分方便,父子进程间通信也比较容易,但缺陷是:每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlinkclose,比较麻烦。可以直接使用匿名映射来代替。linux提供了相应的方法,无需依赖一个文件即可创建映射区,同样需要借助标志位参数flags来指定:

使用MAP_ANONYMOUS(或MAP_ANON),如:

1
int*p = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,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
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>

int var = 100;
int main()
{
int* p;
pid_t pid;

p = (int*)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0); //共享
// p = (int*)mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_PRIVATE,fd,0); //进程各自独占
if(p == MAP_FAILED) //注意:不是p==NULL
{
perror("mmap error");
exit(1);
}

pid = fork();
if(pid == 0) //创建子进程
{
*p = 2000; //给映射区赋值
var = 1000;
printf("child, *p = %d, var = %d\n",*p, var);
} else{
sleep(1);
printf("parent, *p = %d, var = %d\n",*p, var);
wait(NULL);

int ret = munmap(p,4); //释放映射区
if(ret == -1)
{
perror("munmap error");
exit(1);
}
}
}

注意:MAP_ANONYMOUSMAP_ANON两个宏是linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立:

1
2
fd = open("dev/zero",O_RDWR);
p = mmap(NULL, size,PROT_READ|PROT_WRITE,MMAP_SHARED,fd,0);

/dev/zero-文件白洞, 里面有无限量的’\0’, 要多少有多少

/dev/null-文件黑洞, 可以写入任意量的数据

所以在创建映射区时可以用zero文件, 就不用自己创建文件然后拓展大小了

mmap无血缘关系进程间通信

实质上mmap是内核借助文件帮助创建的一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信,只要设置相应的标志位参数flags即可。若想实现共享,应该使用MAP_SHARED

要点:必须是同一个文件

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
//mmap_w.c 写进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

struct STU{
int id;
char name[20];
char sex;
};

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

int main(int argc, char* argv[])
{
int fd;
struct STU student = {10,"xiaoming",'m'};
char* mm;

if(argc < 2)
{
printf("./a.out file_shared\n");
exit(-1);
}
/*打开或创建一个文件*/
fd = open(argv[1],O_RDWR|O_CREAT,0644);
if(fd == -1)
{
sys_err("open error");
}
ftruncate(fd,sizeof(student));
/*建立内存映射区*/
mm = mmap(NULL, sizeof(student),PROT_READ|PROT_WRITE, MAP_SHARED,fd,0);
if(mm == MAP_FAILED)
{
sys_err("mmap error");
}
close(fd);

/*循环使用memcpy向内存映射区中写入数据,并修改stu的id值*/
while(1)
{
memcpy(mm,&student,sizeof(student));
student.id ++;
sleep(2);
}
/*归还内存映射区给内存池*/
munmap(mm,sizeof(student));
}
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
//mmap_r.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

struct STU{
int id;
char name[20];
char sex;
};

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

int main(int argc, char* argv[])
{
int fd;
struct STU student;
struct STU* mm;

if(argc < 2)
{
printf("./a.out file_shared\n");
exit(-1);
}
/*建立内存映射区*/
fd = open(argv[1],O_RDONLY);
if(fd == -1)
{
sys_err("open error");
}
/*循环读出内存映射区中的数据*/
mm = mmap(NULL, sizeof(student),PROT_READ, MAP_SHARED,fd,0);
if(mm == MAP_FAILED)
{
sys_err("mmap error");
}
close(fd);

while(1)
{
printf("id = %d\tname=%s\t%c\n",mm->id,mm->name,mm->sex);
sleep(2);
}
}

无血缘关系进程间通信, 不能用匿名映射

strace: 追踪一个可执行文件在执行过程中所有的系统调用。