linux--进程间通信(管道与系统V IPC)

进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。这使得一个程序能够在同一时间里处理许多用户的要求。因为即使只有一个用户发出要求,也可能导致一个操作系统中多个进程的运行,进程之间必须互相通话。IPC接口就提供了这种可能性。每个IPC方法均有它自己的优点和局限性,一般,对于单个程序而言使用所有的IPC方法是不常见的。
  
进程间通信主要包括管道(pipe), 系统IPC(包括消息队列,信号,共享存储), 网络套接字(socket).

进程间通信目的

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

进程通过与内核及其它进程之间的互相通信来协调它们的行为。Linux支持多种进程间通信(IPC)机制,信号和管道是其中的两种。除此之外,Linux还支持System V 的IPC机制(用首次出现的Unix版本命名)。

进程间通信的种类

  • 管道
    匿名管道pipe
    命名管道
  • System V IPC
    System V 消息队列
    System V 共享内存
    System V 信号量
  • POSIX IPC
    消息队列
    共享内存
    信号量
    互斥量
    条件变量
    读写锁
    本文章也只记录和讨论 管道和System V的IPC机制,因为后面的POSIX IPC还没学!

管道

普通的Linux shell都允许重定向,而重定向使用的就是管道。例如:

$ ps aux | grep communication             // |就是管道符  

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”,相当于是内核为进程间通信而创建的一块缓冲区
在这里插入图片描述
解释:把命令ps aux(列出进程)的输出通过管道连接到命令grep 的标准输入上,最后,命令grep的标准输出在缺省打印机上打印出结果。进程感觉不到这种重定向,它们和平常一样地工作。正是shell建立了进程之间的临时管道。
管道特征:
管道是单向的、先进先出的、无结构的、固定大小的字节流
在这里插入图片描述

  • 它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的一端写入数据,读进程在管道的另一端读出数据。数据读出后将从管道中移走,其它读进程都不能再读到这些数据。
  • 管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
  • 管道中读取数据和写数据非常相似。Linux允许进程无阻塞地读文件或管道(依赖于它们打开文件或者管道的模式),这时,如果没有数据可读或者管道被锁定,系统调用会返回一个错误。这意味着进程会继续运行。另一种方式是阻塞读,即进程在管道I节点的等待队列中等待,直到写进程完成。
  • 如果所有的进程都完成了它们的管道操作,则管道的I节点和相应的共享数据页会被废弃。

管道的实现与本质

在Linux中,使用两个file数据结构来实现管道。这两个file数据结构中f_inode(f_dentry)指针指向同一个临时创建的VFS I节点,而该VFS I节点本身又指向内存中的一个物理页,如下图所示。两个file数据结构中的f_op指针指向不同的文件操作例程向量表:一个用于向管道中写,另一个用于从管道中读。这种实现方法掩盖了底层实现的差异,从进程的角度来看,读写管道的系统调用和读写普通文件的普通系统调用没什么不同。当写进程向管道中写时,字节被拷贝到了共享数据页,当读进程从管道中读时,字节被从共享页中拷贝出来。

摘要:

  • Virtual File System(或者被称为Virtual Filesystem Switch)是Linux内核中的一个软件层,用于给用户空间的程序提供文件系统接口
  • 超级块、目录项、索引节点和文件对象是VFS虚拟文件系统的4个最关键组成要素,一个inode索引节点唯一的对应一个目录文件夹或者文件
  • linux下一切皆是文件
    图片来源于网络
    在这里插入图片描述

匿名管道

用于具有亲缘关系的进程(即子进程和父进程之间的通信)

#include <unistd.h>
int pipe(int fd[2]);   //创建一匿名管道
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回-1

在这里插入图片描述
创建匿名管道来实现父子进程之间的通信
在这里插入图片描述

匿名管道读写规则

1.当没有数据可读(管道为空)时
O_NONBLOCKO disable(阻塞):read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable(非阻塞):read调用返回-1,errno值为EAGAIN。
2.当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
3.当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
4.当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性
补充:
-O_NONBLOCK :非阻塞状态的描述
disable使无效 enable使可能
-PIPE_BUF:为匿名管道的大小 64k
-原子性:当前的操作不能被打断,即运行结果只能是 要么操作完成(1),要么是操作未完成(0),而没有完成一部分的说法。

管道中读取数据和写数据非常相似。Linux允许进程无阻塞地读文件或管道(依赖于它们打开文件或者管道的模式),这时,如果没有数据可读或者管道被锁定,系统调用会返回一个错误。这意味着进程会继续运行。另一种方式是阻塞读,即进程在管道I节点的等待队列中等待,直到写进程完成。
在这里插入图片描述

文件描述符属性的设置

    #include <unistd.h>
    #include <fcntl.h>
    int fcntl(int fd, int cmd, ... /* arg */ );

通过fcntl可以改变已打开的文件性质。fcntl针对描述符提供控制。参数fd是被参数cmd操作的描述符。针对cmd的值,fcntl能够接受第三个参数int arg。
fcntl的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。
下列四个命令有特定返回值
F_DUPFD、F_GETFD、F_GETFL、F_GETOWN.第一个返回新的文件描述符,接下来的两个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。

在这里插入图片描述

获取当前文件描述符的属性
int falgs=fcntl(1,F_GETFL,0);
flags |=O_NONBLOCK;

命名管道

命名管道
匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件
在这里插入图片描述
命名管道的创建
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

$ mkfifo filename

命名管道也可以从程序里用函数创建,相关函数有:

#include <sys/types.h>
#include <sys/stat.h>
 int mkfifo(const char *pathname, mode_t mode);
 

如:

int main(int argc, char *argv[])
{
	mkfifo("myfifo", 0644);    
	return 0;
}

P :管道文件类型
myfifo就是创建出来的命名管道文件
在这里插入图片描述
命名管道的打开规则
如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

例子1-用命名管道实现文件拷贝
第一步:创建命名管道,读取文件,写入命名管道:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include<sys/type.h>
#include<sys/stat.h>
#include<fcntl.h>
int main(int argc, char *argv[])
{
	mkfifo("tp", 0644);//创建命名管道
	int infd;
	infd = open("abc", O_RDONLY);    //打开文件 准备读,再写入命名管道
	if (infd == -1) perror("open");
	int outfd;
	outfd = open("tp", O_WRONLY);    //打开管道文件
	if (outfd == -1) perror("open");
	char buf[1024];
	int n;
	while ((n=read(infd, buf, 1024))>0)  //读
	{
		write(outfd, buf, n);  //写入
	}
	close(infd);
	close(outfd);
	return 0;
}

第二步:读取管道,写入目标文件

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include<sys/type.h>
#include<sys/stat.h>
#include<fcntl.h>
int main(int argc, char *argv[])
{
	int outfd;
	outfd = open("obj", O_WRONLY | O_CREAT | O_TRUNC, 0644); //打开目标文件obj
	if (outfd == -1) 
		perror("open");
		return 1;
	int infd;
	infd = open("tp", O_RDONLY); //打开命名管道文件
	if (outfd == -1)
		perror("open");
		return 1;
	char buf[1024];
	int n;
	while ((n=read(infd, buf, 1024))>0)  //读数据到buf里
	{
		write(outfd, buf, n);   //再由目标文件描述符outfd写入
	}
	close(infd);
	close(outfd);
	unlink("tp");  //删除它的链接数
	return 0;
}

例子2-用命名管道实现server&client通信
客户端写,服务器读
服务器端server从管道里读数据

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
	umask(0);
	if(mkfifo("mypipe", 0644) < 0){  //创建命名管道文件
		perror("mkfifo");
	}
	int rfd = open("mypipe", O_RDONLY);  //打开管道文件 文件描述符为rfd
		if(rfd < 0){
		perror("open");
	}
	char buf[1024];
	while(1){
		buf[0] = 0;
		printf("Please wait...\n");
		ssize_t s = read(rfd, buf, sizeof(buf)-1);//从管道文件里读数据
		if(s > 0 ){
			buf[s-1] = 0;
			printf("client say# %s\n", buf); 
		}else if(s == 0){
			printf("client quit, exit now!\n");
			exit(EXIT_SUCCESS);
		}else{
			perror("read");
		}
	}
	close(rfd);
	return 0;
}

客户端client写数据到管道

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
	int wfd = open("mypipe", O_WRONLY);//打开管道文件
	if(wfd < 0){
		perror("open");
	}
	char buf[1024];
	while(1){
		buf[0] = 0;
		printf("Please Enter# ");
		fflush(stdout);  //刷新缓冲区
		ssize_t s = read(0, buf, sizeof(buf)-1); //从标准输入(键盘)里写到buf中
		if(s > 0 ){
			buf[s] = 0;
			write(wfd, buf, strlen(buf)); //再写入管道文件
		}else if(s <= 0){
			perror("read");
		}
	}
	close(wfd);
	return 0;
}

演示结果:

在这里插入图片描述
在这里插入图片描述
管道缺点:
管道及命名管道是典型的随进程持续IPC,并且,只能传送无格式的字节流无疑会给应用程序开发带来不便,另外,它的缓冲区大小也受到限制。

System V IPC

System V 共享内存

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。
在这里插入图片描述
创建共享内存&通信流程
1.先在物理内存当中开辟一段空间
2.各个进程通过页表结构将物理内存映射到自己的虚拟地址空间当中的共享区
3.各个进程之间的通信是通过修改自2虛拟地址空间当中的共享区的地址来完成的
特性:
不同的进程对共享内存区域进行读的时候,并不会”读走“物理内存当中的数据

创建共享内存&使用共享内存的接口
1.创建共享内存

int shmget(key_t key, size. t size, int shmflag)
key :共享内存的标识符(名字)
size :共享内存的大小
shmflag: .
	IPC_ CREAT:如果想获取的共享内存不存在,则创建共享内存。如果存在则返回共享内存操作句柄
	IPC. EXCL |IPC. CREAT :如果想获取的共享内存存在,则报错
	按位或上权限,可以使用8进制的数字来进行参数的按位或
返回值:成功返回共享内存的操作句柄

2.将进程附加到共享内存上

void* shmat(int shmid, const void* shmaddr, int shmflag)
shmid:共享内存的操作句柄(地址)
shmaddr:程序员去指定映射到共享区的哪一个地址 ,程序员-般都不会去选择,设置为NULL ,由操作系统来指定将内存映射到哪一个地址上
shmflag:
	0:可读可写
	IPC_ RDONLY:只读
返回值:成功返回一个指针,该指针指向映射到共享区的那一块地址 ,程序员就可以操作这个地址来对物理内存区域进行读写操作,失败返回-1

3.从共享内存当中分离进程

int shmdt(const void* shmaddr)
shmaddr:共享区当中映射的物理内存的首地址, shmat返回的指针

4.共享内存的销毁

int shmct(int shmid, int cmd, struct shmid_ds* buf)
shmid:共享内存的操作句柄
cmd(三个可取值)
	销毁:
		IPC_ RMID:删除共享内存,标记共享内存为删除状态
	获取共享内存信息
		IPC_ STAT:获取共享内存状态,需要搭配struct shmid ds
		IPC_SET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值
buf:作为-个出参,用来获取共享内存状态信息,传入-个struct shmid. ds结构体对象的地址

共享内存的特点
1.共享内存就是使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。
2.共享内存本身没有进行同步与互斥!但往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
3.其他进程能把同一段共享内存段“连接到(映射)”他们自己的地址空间里去。所有进程都能访问共享内存中的地址。如果一个进程向这段共享内存写了数据,所做的改动会即时被有访问同一段共享内存的其他进程看到。
4.共享内存的使用大大降低了在大规模数据处理过程中内存的消耗,但是共享内存的使用中有很多的陷阱(不小心删除了附有进程的共享内存),一不注意就很容易导致程序崩溃。

使用共享内存实现client与server通信

server在共享内存里读取数据

#include<stdio.h>                                                                                                                            
    2 #include<unistd.h>
    3 #include<stdlib.h>
    4 #include<sys/shm.h>
    5 #define _key 0X6666666
    6 int main()
    7 {
    8   int shmid=shmget(_key,1024,IPC_CREAT|0664);//创建共享内存
    9   if(shmid<0)
   10   {
   11     perror("shmget");
   12     return 1;
   13   }
   14   void *p= shmat(shmid,NULL,0);   //连接 权限为可读可写
   15   if(!p)
   16   {
   17     perror("shmat");
   18     return 1;
   19   }
   20   int i=0;
   21   sleep(2);
   22   while(i<10)
   23   {
   24     printf("client## %s\n",p);//读取client写入的数据
   25     sleep(1);
   26     i++;
   27   }
   28   if(shmdt(p)<0)  //脱离失败返回-1
    {
   30     perror("shmdt");
   31     return -1;
   32   }
W> 33   if(shmctl(shmid,IPC_RMID,NULL)<0);//销毁
   34   {
   35     perror("shmctl");
   36     return -1;
   37   }
   38   
   39   return 0;                                                                                                                                  
   40 }

client往共享内存写入数据

#include<stdio.h>
    2 #include<unistd.h>
    3 #include<sys/shm.h>
    4 #include<stdlib.h>
    5 #define _key 0X6666666
    6 int main()
    7 {
    8   int shmid=shmget(_key,1024,IPC_CREAT|0664);//根据_key找到之前创建的那一块内存 
    9   if(shmid<0)
   10   {
   11     perror("shmget");
   12     return -1;
   13   }
E> 14   char* p=shmat(shmid,NULL,0); //连接共享内存
   15   int i=0;
   16                                                                                                                                              
   17   while(i<10)   //写数据
   18   {
   19     p[i]=i+'A';
   20     i++;
   21     sleep(1);
   22   }
   23   p[i]=0;
   24   shmdt(p);//释放
   25   sleep(1);
   26 
   27 
   28   return 0;

在这里插入图片描述
在这里插入图片描述
共亨内存的生命周期
共享内存的生命周期是眼随操作系统内核的

ipcs:可以查看共享内存
ipcs -m
ipcrm -m [shmid]  //删除共享内存

如果删除了一个有进程附加的共享内存,操作系统的流程是:
1.先标记当前的共享内存为destroy状态,并且将key设置为0X00000000 ,表示当前的共享内存不能两被其他进程所附加,同时会释放共享内存
2.带来的风险:导致正在附加到该共享内存上的进程有崩溃的风险. 一般禁止删除有进程附加的共享内存。
3.当进程退出的时候,操作系统就会将描述共享内存的结构体也释放掉
在这里插入图片描述

System V 消息队列

“消息队列”是在消息的传输过程中保存消息的容器。消息队列管理器在将消息从它的源中继到它的目标时充当中间人。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。
消息队列的特性:

  • 先进先出,底层的实现是链表,在内核当中创建。
  • 在队列当中每一个元素都有自己的类型 ,类型之间有一个优先级的概念。
  • 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
  • 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
  • IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核。
  • 消息队列可以进行双工通信

属性:
系统V消息队列是随内核持续的,只有在内核重起或者显示删除一个消息队列时,该消息队列才会真正被删除。因此系统中记录消息队列的数据结构(struct ipc_ids msg_ids)位于内核中,系统中的所有消息队列都可以在结构msg_ids中找到访问入口。 消息队列就是一个消息的链表。每个消息队列都有一个队列头,用结构struct msg_queue来描述。队列头中包含了该消息队列的大量信息,包括消息队列键值、用户ID、组ID、消息队列中消息数目等等,甚至记录了最近对消息队列读写进程的ID。读者可以访问这些信息,也可以设置其中的某些信息。
消息队列操作函数
队列当中所有消息的长度之和16384
系统当中最大的队列数2379
函数参数有些和分享内存的性质一样

int msgget(key_t key, int msgflg)
key消息队列的标识符
msgflg:
	IPC_CRETA
	IPC_CRETA |IPC_ EXCL
	按位或上权限		
int msgsnd(int msqid, const void *msgp,size_t msgsz, int msgflg);
msgid消息队列的操作句柄
msgp发送的数据
msgsz:数据的长度
msgflg:
	0:当队列满了,则阻塞
	IPC_NOWAIT:如果说队列满了,则当前发送的操作不会进行阻塞.函数返回
ssize_ t msgrev(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg); 
msgtype:
	0:表示什么类型都可以按收
	msgtype > 0: 则返回队列当中消息类型为msgtype的第一个消息
	msgtype < 0:则返回队列当中消息类利小干等Fmsgtype绝对何的消息
	如果这样的消息比较多,则返回类型最小的那个消息
int msgctl(int msgid, int cmd, struct msgid ds* buf)
cmd
	IPC_ STAT
	IPC_ SET
	IPC_RMID

System V 信号量

本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。信号量,分为互斥信号量,和条件信号量。一般说来,为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量;
(2)若此信号量的值为正,则允许进行使用该资源,进程将信号量减去所需的资源数;
(3)若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1);
(4)当进程不再使用一个信号量控制的资源时,信号量值加其所占的资源数,如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
使用机制与规则
信号量(Semaphores)机制是一种卓有成效的进程同步工具,在长期广泛的应用中,信号量机制得到了极大的发展,它从整型信号量经记录型信号量,进而发展成为“信号量集机制”,信号量机制已经被广泛的应用到单处理机和多处理机系统以及计算机网络中。
信号量S是一个整数,S大于等于零是代表可供并发进程使用的资源实体数,当S小于零时则表示正在等待使用临界区的进程数
对信号量操作的PV原语
P原语操作的动作是:
(1)S减1;
(2)若S减1后仍大于或等于零,则进程继续执行;
(3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转进程调度。
V原语操作的动作是:
(1)S加1;
(2)若相加结果大于零,则进程继续执行;
(3)若相加结果小于或等于零,则从该信号的等待队列中唤醒一等待进程,然后再返回原进程继续执行或转进程调度。
注意:PV操作对于每一个进程来说,都只能进行一次,而且必须成对使用。在PV原语执行期间不允许有中断的发生。

发布了55 篇原创文章 · 获赞 152 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_44785014/article/details/104986782
今日推荐