一、什么是进程间通信?

Linux环境下,进程地址空间是相互独立的,每个进程有各自独立的用户地址空间,进程之间的全局变量在另一个进程中都看不到,要交换数据必须通过内核。进程1把数据写入内核的一个缓冲区,另一个进程可以从内核缓冲区读走,内核提供的这种机制就是进程间通信IPC(InterProcess Communication)。

二、常用进程间通信方式

1. 管道-pipe

①管道也称匿名管道,应用于有血缘关系的进程通信。


管道特质:
(1) 管道本质是一块内核缓冲区。
(2) 由两个文件描述符引用,一个读端,一个写端。
(3) 数据从写端流入,读端流出。
(4) 当两个进程都终结的时候,管道也自动消失。
(5) 管道的读端和写端都是默认阻塞的。

②管道的原理:

管道的实质是内核缓冲区,内部使用环形队列实现,默认缓冲区大小为4K,可以使用ulimit -a命令获取大小,实际操作过程中缓冲区会根据数据压力做适当调整。

③管道的局限性:

(1) 不可以反复读取,数据一旦读走便不在管道中存在。
(2) 数据只能单向流动,需要两个管道才能实现双向流动。
(3) 只能在有血缘关系的进程间使用管道。因为没有血缘关系的进程无法获取管道描述符。

④创建管道:

使用pipe函数创建管道:函数原型:

  int pipe(int fd[2]);
  若函数调用成功,返回0,fd[0]存放管道的读端,fd[1]存放管道的写端。
  调用失败返回-1,并设置errno值。

⑤管道实例:

父子进程间通信:
(1) 父进程创建管道

(2) 父进程fork出子进程

(3) 父进程关闭fd[0],子进程关闭fd[1]

代码:

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

//创建子进程
pid_t pid = fork();
if(pid<0) 
{
	perror("fork error");
	return -1;
}
else if(pid>0)
{
	//关闭读端
	close(fd[0]);
	sleep(5);
	write(fd[1], "hello world", strlen("hello world"));	

	wait(NULL);
}
else 
{
	//关闭写端
	close(fd[1]);
	
	char buf[64];
	memset(buf, 0x00, sizeof(buf));
	int n = read(fd[0], buf, sizeof(buf));
	printf("read over, n==[%d], buf==[%s]\n", n, buf);

}

return 0;
  }

⑥管道的读写行为:

  1. 读操作:
  • 有数据:read正常读,返回读出的字节数
  • 无数据:写端全部关闭
    • read解除阻塞,返回0,相当于读文件读到了尾部
    • 没有全部关闭
    • read阻塞
  1. 写操作:
  • 读端全部关闭:管道破裂,进程终止,内核给当前进程发SIGPIPE信号
  • 读端没全部关闭:
    • 缓冲区写满了:write阻塞
    • 缓冲区没有满:继续write

⑦如何设置管道为非阻塞:

默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参
考下列三个步骤进行:
第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

    int main()
    {
          //创建管道
          //int pipe(int pipefd[2]);
          int fd[2];
          int ret = pipe(fd);
          if(ret<0)
          {
                perror("pipe error");
                return -1;
          }
          printf("pipe size==[%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF));
          printf("pipe size==[%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF));
          //关闭写端
          close(fd[1]);
          //设置管道的读端为非阻塞
          int flag = fcntl(fd[0], F_GETFL);
          flag |= O_NONBLOCK;
          fcntl(fd[0], F_SETFL, flag);      
          char buf[64];
          memset(buf, 0x00, sizeof(buf));
          int n = read(fd[0], buf, sizeof(buf));
          printf("read over, n==[%d], buf==[%s]\n", n, buf);
          return 0;
    }
    

⑧查看管道缓冲区大小:

(1) 命令:ulimit -a
(2) 函数:

  long fpathconf(int fd, int name);
  printf("pipe size==[%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF));
  printf("pipe size==[%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF));

2. FIFO命名管道

①管道只能用于有血缘关系的进程间通信,但是FIFO可以用以不相关的进程间交换数据

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

②创建FIFO命名管道

(1) 方式1:使用命令:mkfifo
格式:mkfifo 管道名
(2) 方式2:使用函数:

  int mkfifo(const char *pathname, mode_t mode);(可以使用man 3 mkfifo命令查看)

当创建了一个FIFO,就可以使用open函数打开它,进行常见的文件I/O操作,如:write、read、close、unlink等
注意:FIFO严格遵循先进先出,都FIFO的读总是从文件开头开始,写总是从末尾开始写入,他们不支持诸如lseek()的文件定位操作

③FIFO完成两个进程通信的示意图:

思路:
进程A:
创建一个fifo文件:myfifo
调用open函数打开myfifo文件
调用write函数写入一个字符串如:“hello world”(其实是将数据写入到了内核缓冲区)
调用close函数关闭myfifo文件

进程B:
调用open函数打开myfifo文件
调用read函数读取文件内容(其实就是从内核中读取数据)
打印显示读取的内容
调用close函数关闭myfifo文件

  //write.c
  int main()
  {
//创建fifo文件
//int mkfifo(const char *pathname, mode_t mode);
int ret = access("./myfifo", F_OK);
if(ret!=0)
{		
	ret = mkfifo("./myfifo", 0777);
	if(ret<0)
	{
		perror("mkfifo error");
		return -1;
	}
}

//打开文件
int fd = open("./myfifo", O_RDWR);
if(fd<0)
{
	perror("open error");
	return -1;
}

//写fifo文件
int i = 0;
char buf[64];
while(1)
{
	memset(buf, 0x00, sizeof(buf));
	sprintf(buf, "%d:%s", i, "hello world");
	write(fd, buf, strlen(buf));
	sleep(1);

	i++;
}

//关闭文件
close(fd);

//getchar();

return 0;
  }

  //read.c
  int main()
  {
//创建fifo文件
//int mkfifo(const char *pathname, mode_t mode);
//判断myfofo文件是否存在,若不存在则创建
int ret = access("./myfifo", F_OK);
if(ret!=0)
{
	ret = mkfifo("./myfifo", 0777);
	if(ret<0)
	{
		perror("mkfifo error");
		return -1;
	}
}

//打开文件
int fd = open("./myfifo", O_RDWR);
if(fd<0)
{
	perror("open error");
	return -1;
}

//读fifo文件
int n;
char buf[64];
while(1)
{
	memset(buf, 0x00, sizeof(buf));
	n = read(fd, buf, sizeof(buf));
	printf("n==[%d], buf==[%s]\n", n, buf);
}

//关闭文件
close(fd);

//getchar();

return 0;
  }

3. 内存映射区

①原理:

存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与内存中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作,也就避免了用户态到内核态的频繁切换,可以降低系统开销。
使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。

②mmap函数

(1)函数作用:建立存储映射区
(2)函数原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
(3)函数返回值:成功返回创建的映射区首地址,失败返回MAP_FAILED宏,设置errno值
(4)参数:

  • addr:指定映射区的开始地址,设为NULL则由系统指定。
  • length:映射到内存的文件长度。
  • prot:映射区的保护方式,最常用的:
    • 读:PROT_READ
    • 写:PROT_WRITE
    • 读写:PROT_READ | PROT_WRITE
  • flags:映射区的特性,可以是:
    • MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享。
    • MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制, 对此区域所做的修改不会写回原文件。
  • fd:由open返回的文件描述符,代表要映射的文件。
  • offset:以文件开始处的偏移量,必须是4K的整数倍,通常为0,即从文件头开始映射。

③munmap函数

  1. 函数作用:释放由mmap函数建立的存储映射区。
  2. 函数原型:int munmap(void *addr, size_t length);
  3. 返回值:成功返回0,失败返回-1,并设置errno值。
  4. 函数参数:
  • addr:调用mmap函数成功返回的映射区首地址
  • length:映射区大小(即:mmap函数的第二个参数)

注意事项:
(1) 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区。
(2) 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
(3) 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。
(4) 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
(5) munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
(6) 文件偏移量必须为0或者4K的整数倍.
(7) mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

④例:

  1. 使用mmap完成父子进程通信:

    int main()
    {
    //使用mmap函数建立共享映射区
    int fd = open("./test.log", O_RDWR);
    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, len, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
    if(addr==MAP_FAILED)
    {
      perror("mmap error");
      return -1;
    }
    close(fd);
    
    //创建子进程
    pid_t pid = fork();
    if(pid<0) 
    {
      perror("fork error");
      return -1;
    }
    else if(pid>0)
    {
      memcpy(addr, "hello world", strlen("hello world"));	
      wait(NULL);
    }
    else if(pid==0)
    {
      sleep(1);
      char *p = (char *)addr;
      printf("[%s]", p);
    }
    
    return 0;
    }```
    
    

示意图:

思路:
(1)调用mmap函数创建存储映射区,返回映射区首地址addr。
(2)调用fork函数创建子进程,子进程也拥有了映射区首地址addr。
(3)父子进程可以通过映射区首地址指针addr完成通信。
(4)调用munmap函数释放存储映射区。

  1. 使用mmap完成没有血缘关系的进程间的通信:
    思路:两个进程打开相同的文件,然后建立存储映射区,这样两个进程共享同一个存储映射区。
    mmap_read.c:
      {
	//使用mmap函数建立共享映射区
	//void *mmap(void *addr, size_t length, int prot, int flags,
    //              int fd, off_t offset);
	int fd = open("./test.log", O_RDWR);
	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);
	if(addr==MAP_FAILED)
	{
		perror("mmap error");
		return -1;
	}

	char buf[64];
	memset(buf, 0x00, sizeof(buf));
	memcpy(buf, addr, 10);
	printf("buf=[%s]\n", buf);

	return 0;
      }

mmap_write.c:

  int main()
  {
//使用mmap函数建立共享映射区
//void *mmap(void *addr, size_t length, int prot, int flags,
//              int fd, off_t offset);
int fd = open("./test.log", O_RDWR);
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);
if(addr==MAP_FAILED)
{
	perror("mmap error");
	return -1;
}

memcpy(addr, "0123456789", 10);

return 0;
  }
  1. 使用mmap函数建立匿名映射:
    mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    例:
    //使用mmap匿名映射完成父子进程间通信
    int main()
    {
    //使用mmap函数建立共享映射区
    void * addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
    if(addr==MAP_FAILED)
    {
      perror("mmap error");
      return -1;
    }
    
    //创建子进程
    pid_t pid = fork();
    if(pid<0) 
    {
      perror("fork error");
      return -1;
    }
    else if(pid>0)
    {
      memcpy(addr, "hello world", strlen("hello world"));	
      wait(NULL);
    }
    else if(pid==0)
    {
      sleep(1);
      char *p = (char *)addr;
      printf("[%s]", p);
    }
    
    return 0;
    }