Linux进程通信之管道

Linux进程间通信的基本思想是:让两个进程看到一份公共的资源!

Linux进程间通信的目的

  • 数据传输:⼀个进程需要将它的数据发送给另⼀个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:⼀个进程需要向另⼀个或⼀组进程发送消息,通知它们发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另⼀个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

通信方式之管道

管道是Unix中最古老的进程间通信的形式。 我们把从⼀个进程连接到另⼀个进程的⼀个数据流称为⼀个”管道”

  • 管道是面向字节流的
  • 管道的生命周期:与进程一致
  • 管道只能用于单向通信
  • 内核会对管道操作进行同步与互斥

接下来看看管道的使用:

mark

匿名管道

pipe函数

功能:创建匿名管道

1#include <unistd.h>
2int pipe(int pipefd[2]);

返回值: On success, zero is returned. On error, -1 is returned, and errno is set appropriately. 成功返回0,失败返回错误代码! 参数说明: pipe() creates a pipe, a unidirectional data channel that can be used for interprocess communication. The array pipefd is used to return two file descriptors referring to the ends of the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe. Data written to the write end of the pipe is buffered by the kernel until it is read from the read end of the pipe. pipe() 创建一个管道,一个可用于进程间通信的单向数据通道。该文件描述符数组用于返回引用管道末端的两个文件描述符。 pipefd [0]指的是管道的读端。 pipefd [1]指的是管道的写端。写入结束的数据管道由内核缓冲,直到从管道的读取端读取。

管道简单使用示例:从键盘读取数据,写⼊管道,读取管道,写到屏幕

 1#include <string.h>
 2#include <unistd.h>
 3#include <stdlib.h>
 4#include <stdio.h>
 5
 6int main(int argc, char *argv[]) {
 7	int fds[2];
 8	char buf[100] = { 0 };
 9	size_t len;
10	ssize_t r_len;
11	
12
13	if (pipe(fds) == -1) {
14		perror("make pipe");
15		exit(1);
16	}
17
18	//read from stdin
19	while (fgets(buf, 100, stdin)) {
20		len = strlen(buf);
21		//write to pipe
22		if (write(fds[1], buf, len) != len) {
23			perror("write to pipe");
24			break;
25		}
26		memset(buf, 0, 100);
27
28		//read from pipe
29		if ((r_len = read(fds[0], buf, (size_t)100)) == -1) {
30			perror("read form pipe");
31			break;
32		}
33
34		//write to stdout
35		if (write(1, buf, len) != len) {
36			perror("write to stdout");
37			break;
38		}
39	}
40		
41	return 0;
42}

亲缘关系进程直接的通信

上面的示例演示了管道的基本使用方式,但是不包含进程之间的通信!接下来看看父子进程之间的通信:

mark

从文件描述符理解管道:

mark

从内核角度理解管道:

mark

所以可以看到,管道其实也是文件,在Linux下一切皆文件!

 1#include <stdio.h>
 2#include <errno.h>
 3#include <string.h>
 4#include <unistd.h>
 5#include <stdlib.h>
 6
 7int main(){
 8    int pipefd[2];
 9    
10    //创建一个匿名管道,失败直接退出
11    if(pipe(pipefd) == -1){
12        perror("pipe");
13        exit(EXIT_FAILURE);
14    }
15
16    pid_t pid;
17    pid = fork();
18    if(pid == -1){
19        perror("pipe");
20        exit(EXIT_FAILURE);
21    }
22
23	 //父进程
24    if(pid == 0){
25        close(pipefd[0]);
26        write(pipefd[1], "hello", 5);
27        close(pipefd[1]);
28        exit(EXIT_SUCCESS);
29    }
30
31    close(pipefd[1]);
32
33    char buf[10] = {0};
34    read(pipefd[0], buf, 10);
35    printf("buf = %s\n", buf);
36
37    return 0;
38}

管道的读写规则

  • 当没有数据可读时
    • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止
    • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN
  • 当管道满的时候
    • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出,这个不难理解,因为没有人从管道读信息时向管道写入东西是没有意义的,这是一种资源的浪费,所以管道读端对应的文件描述符被关闭时其实应该结束掉write进程;
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道的特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

命名管道

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件! 接下来使用一个脚本来演示用管道进行通信:

如果此时我们关闭读端,那么写端就会退出,道理很简单,和匿名管道一样,如果读端都关闭了那么此时如果写端还在写的话其实是一种资源浪费,于是操作系统直接向写端发信号终止写端进程!

匿名管道和命名管道之间的区别

  • 匿名管道由pipe函数创建并打开,命名管道由mkfifo函数创建,打开用open函数
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同

命名管道的打开规则

如果当前打开操作是为读而打开FIFO时:

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO 如果当前打开操作是为写而打开FIFO时:
  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

接下来是一个模拟客户端和服务器端通信的示例:

mark

client.c

 1#include <stdio.h>
 2#include <sys/stat.h>
 3#include <sys/types.h>
 4#include <fcntl.h>
 5#include <unistd.h>
 6#include <memory.h>
 7
 8#define FIFONAME "mypipe"
 9
10int main() {
11    //创建一个命名管道
12    mkfifo(FIFONAME, 0664);
13
14    //打开这个命名管道
15    int fd = open(FIFONAME, O_WRONLY);
16
17    if(fd < 0){
18        return 1;
19    }
20
21    char buf[1024];
22    while(1){
23        printf("Please Enter Your Message To Server# ");
24        fflush(stdout);
25        //从标准输入读取信息
26        ssize_t s = read(0,buf,sizeof(buf));
27        buf[s-1] = 0;//覆盖掉之前的回车换行
28        
29        //向管道写
30        write(fd,buf,strlen(buf));
31    }
32    close(fd);
33    return 0;
34}

server.c

 1#include <stdio.h>
 2#include <sys/stat.h>
 3#include <sys/types.h>
 4#include <fcntl.h>
 5#include <unistd.h>
 6
 7#define FIFONAME "mypipe"
 8
 9int main() {
10    //创建一个命名管道
11    mkfifo(FIFONAME, 0664);
12
13    //打开这个命名管道
14    int fd = open(FIFONAME, O_RDONLY);
15
16    if(fd < 0){
17        return 1;
18    }
19
20    char buf[1024];
21    while(1){
22        ssize_t s = read(fd, buf, sizeof(buf)-1);
23        if(s > 0){
24            //读取成功
25            buf[s] = 0;
26            printf("client# %s\n", buf);
27        }else if(s == 0){
28            //写端把文件描述符关闭
29            printf("client quit!server quit too!\n");
30            break;
31        }else{
32            break;
33        }
34    }
35    return 0;
36}

Bash中的管道是采用匿名管道的方式进行通信,由于匿名管道只能用于在具有亲缘关系之间通信,但是由Bash开启的进程之间是属于兄弟关系,自然就可以通过匿名管道进行通信,多个进程(多个管道)的情况下只要每个进程关闭或打开相应的读写端,形成链式数据结构便可以进行通信了(再次佩写服Bash的大佬)