Linux进程间通信
2022-02-01 # 学习笔记 # Linux

Linux进程间通信(IPC)

需求1:

实现:

用兄弟进程以及管道通信(无名)来实现命令 ls | wc -l

ls | wc -l命令使用了管道,将ls命令的输出作为wc命令的输入。

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

int main(int argc, char* argv[]){

int i;
//创建管道
int fd[2];
int res = pipe(fd);
if (res == -1)perror("pipe error");

//循环创建子进程
for (i = 0; i < 2; i++) {
pid_t pid;
pid = fork();
if (pid == 0)break;
}


if (i == 2){ //父进程
//关闭管道的读端和写端
close(fd[0]);
close(fd[1]);
wait(NULL);
wait(NULL);
}else if (i == 0){ //兄进程
//关闭管道的读端
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);

}
else if (i == 1) {//弟进程
//关闭管道的写端
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
}
return 0;
}

补充:

linux中父子进程的文件描述符是共享的。因此实现父子进程或兄弟进程通信时,可以使用pipe(无名管道),相当于父进程用fork的方式,将管道的读端和写端传给了子进程。但只能用于有血缘关系的进程之间的通信。

在使用时需要关闭进程的读端或写端,确保有且仅有一个进程持有管道的写端,另一进程持有管道的读端。否则可能会产生阻塞。

管道机制:

读管道:
  • 管道有数据:read 返回实际读到的字节数。

  • 管道无数据:

​ 1)无写端,read 返回 0 (类似读到文件尾)

​ 2)有写端,read 阻塞等待。

写管道:
  • 无读端: 异常终止。 (SIGPIPE信号导致的)

  • 有读端:

​ 1) 管道已满, 阻塞等待

​ 2) 管道未满, 返回写出的字节个数。

分析

从上述机制不难分析,当有非写入程序持有管道的写入端的时候,可能会造成后续读端一直阻塞。因此要通过关闭不同进程的读端和写端来明确管道的方向,防止阻塞。

需求2:

采用管道来实现无血缘关系的进程之间的通信。

思路:

linux中”一切皆文件“,管道也不例外。pipe()函数所创建的管道没有名称,一般被称为无名管道,但这种管道只能用于有血缘关系的进程间使用。而另一种管道——FIFO,常被称为命名管道来区分pipe,通过FIFO,无血缘关系的进程也可以进行通信。

FIFO是linux基础文件类型中的一种,在目录中可以看到,但在磁盘上并没有数据块,并不占用磁盘空间,仅仅用来标识通往内核的一条通道,不同的进程通过打开这个文件,来进行read和write。实际上是在读写内核通道,从而实现了进程间通信。

命名管道的创建方式

命令:

1
mkfifo 管道名

库函数:

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

成功返回0,失败返回-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
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>

int main(int argc, char* argv[])
{
char buf[4096];

printf("I'm write\n");

//判断是否通过控制台参数传入了一个管道
if (argc < 2) {
printf("Enter like this: ./xxx.out fifoname\n");
return -1;
}

//打开管道
int fd = open(argv[1], O_WRONLY);
//向管道中持续写入数据
int i = 0;
while (i < 30) {
sprintf(buf, "this is the %dth message\n",i++);
write(fd, buf, strlen(buf));
sleep(1);
}
close(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
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char* argv[]) {

printf("I'm read\n");

//判断是否通过控制台参数传入了一个管道
if (argc < 2) {
printf("enter like this: ./xxx.out fifoname\n");
return -1;
}

//打开管道
int fd = open(argv[1], O_RDONLY);

//从管道中读取数据
char buf[4096];

int len;
while (1) {
len = read(fd, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
sleep(1);
}

close(fd);
return 0;

}

效果实例

补充:

  • sprintf函数可以将字符串写入到指定的缓冲区中,写入的内容为“字符串+/0”,每次写入结束后都会确保末尾为/0,因此在每次写入前也可以不清除缓冲区中已有的字符串。
  • 在打开管道的时候应注意权限的控制,读入端O_RDONLY,写入端O_WRONLY,权限设置错误可能会造成open函数阻塞。