进程间通信方式总结

    科技2022-07-16  109

    目录

    1 进程间通信的定义

    2 管道

    2.1 无名管道

    2.2 FIFO

    3 信号(Signal)

    4 消息(Message)队列

    5 共享内存(share memory)

    6 信号量(semaphore)

    7 套接字(socket)


    1 进程间通信的定义

    进程间通信就是在不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都可以访问的介质呢?进程的用户空间是互相独立的,一般而言是不能互相访问的,唯一的例外是共享内存区。另外,系统空间是“公共场所”,各进程均可以访问,所以内核也可以提供这样的条件。此外,还有双方都可以访问的外设。在这个意义上,两个进程当然也可以通过磁盘上的普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义上这也是进程间通信的手段,但是一般都不把这算作“进程间通信”。

    进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。IPC方法包括管道(PIPE)、消息队列、信号、共用内存以及套接字(Socket)。

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

     IPC目的

    1)数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。

    2)共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

    3)通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

    4)资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

    5)进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

    IPC方式包括:管道、系统IPC(信号量、消息队列、共享内存)和套接字(socket)。

     

    通信方法无法介于内核态与用户态的原因管道(不包括命名管道)局限于父子进程间的通信。消息队列在硬、软中断中无法无阻塞地接收数据。信号量无法介于内核态和用户态使用。共享内存需要信号量辅助,而信号量又无法使用。

    2 管道

    2.1 无名管道

    管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。

    特点:

    它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。

    它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。

    它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

    原型:

    #include <unistd.h> int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1

    当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。如下图:

    要关闭管道只需将这两个文件描述符关闭即可。

    例子

    单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。如下图所示

    若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。

    #include<stdio.h> #include<unistd.h> int main() { int fd[2]; // 两个文件描述符 pid_t pid; char buff[20]; if(pipe(fd) < 0) // 创建管道 printf("Create Pipe Error!\n"); if((pid = fork()) < 0) // 创建子进程 printf("Fork Error!\n"); else if(pid > 0) // 父进程 { close(fd[0]); // 关闭读端 write(fd[1], "hello world\n", 12); } else { close(fd[1]); // 关闭写端 read(fd[0], buff, 20); printf("%s", buff); } return 0; }

    2.2 FIFO

    FIFO,也称为命名管道,它是一种文件类型。

    1、特点

    FIFO可以在无关的进程之间交换数据,与无名管道不同。

    FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

    2、原型

    #include <sys/stat.h> // 返回值:成功返回0,出错返回-1 int mkfifo(const char *pathname, mode_t mode);

    其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。

    当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

    若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。

    若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

    3、例子

    FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。下面的例子演示了使用 FIFO 进行 IPC 的过程:

    write_fifo.c

    #include<stdio.h> #include<stdlib.h> // exit #include<fcntl.h> // O_WRONLY #include<sys/stat.h> #include<time.h> // time int main() { int fd; int n, i; char buf[1024]; time_t tp; printf("I am %d process.\n", getpid()); // 说明进程ID if((fd = open("fifo1", O_WRONLY)) < 0) // 以写打开一个FIFO { perror("Open FIFO Failed"); exit(1); } for(i=0; i<10; ++i) { time(&tp); // 取系统当前时间 n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp)); printf("Send message: %s", buf); // 打印 if(write(fd, buf, n+1) < 0) // 写入到FIFO中 { perror("Write FIFO Failed"); close(fd); exit(1); } sleep(1); // 休眠1秒 } close(fd); // 关闭FIFO文件 return 0; }

    read_fifo.c

    #include<stdio.h> #include<stdlib.h> #include<errno.h> #include<fcntl.h> #include<sys/stat.h> int main() { int fd; int len; char buf[1024]; if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 创建FIFO管道 perror("Create FIFO Failed"); if((fd = open("fifo1", O_RDONLY)) < 0) // 以读打开FIFO { perror("Open FIFO Failed"); exit(1); } while((len = read(fd, buf, 1024)) > 0) // 读取FIFO管道 printf("Read message: %s", buf); close(fd); // 关闭FIFO文件 return 0; }

    在两个终端里用 gcc 分别编译运行上面两个文件,可以看到输出结果如下:

    [cheesezh@localhost]$ ./write_fifo I am 5954 process. Send message: Process 5954's time is Mon Apr 20 12:37:28 2015 Send message: Process 5954's time is Mon Apr 20 12:37:29 2015 Send message: Process 5954's time is Mon Apr 20 12:37:30 2015 Send message: Process 5954's time is Mon Apr 20 12:37:31 2015 Send message: Process 5954's time is Mon Apr 20 12:37:32 2015 Send message: Process 5954's time is Mon Apr 20 12:37:33 2015 Send message: Process 5954's time is Mon Apr 20 12:37:34 2015 Send message: Process 5954's time is Mon Apr 20 12:37:35 2015 Send message: Process 5954's time is Mon Apr 20 12:37:36 2015 Send message: Process 5954's time is Mon Apr 20 12:37:37 2015 [cheesezh@localhost]$ ./read_fifo Read message: Process 5954's time is Mon Apr 20 12:37:28 2015 Read message: Process 5954's time is Mon Apr 20 12:37:29 2015 Read message: Process 5954's time is Mon Apr 20 12:37:30 2015 Read message: Process 5954's time is Mon Apr 20 12:37:31 2015 Read message: Process 5954's time is Mon Apr 20 12:37:32 2015 Read message: Process 5954's time is Mon Apr 20 12:37:33 2015 Read message: Process 5954's time is Mon Apr 20 12:37:34 2015 Read message: Process 5954's time is Mon Apr 20 12:37:35 2015 Read message: Process 5954's time is Mon Apr 20 12:37:36 2015 Read message: Process 5954's time is Mon Apr 20 12:37:37 2015

    上述例子可以扩展成 客户进程—服务器进程 通信的实例,write_fifo的作用类似于客户端,可以打开多个客户端向一个服务器发送请求信息,read_fifo类似于服务器,它适时监控着FIFO的读端,当有数据时,读出并进行处理,但是有一个关键的问题是,每一个客户端必须预先知道服务器提供的FIFO接口,下图显示了这种安排:

    3 信号(Signal)

    信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,而无需知道该进程的状态。如果该进程当前并未处于执行状态,则该信号就有内核保存起来,知道该进程回复执行并传递给它为止。如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程。

    Linux系统中常用信号: (1)SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。 (2)SIGINT:程序终止信号。程序运行过程中,按Ctrl+C键将产生该信号。 (3)SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+\\键将产生该信号。 (4)SIGBUS和SIGSEGV:进程访问非法地址。 (5)SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。 (6)SIGKILL:用户终止进程执行信号。shell下执行kill -9发送该信号。 (7)SIGTERM:结束进程信号。shell下执行kill 进程pid发送该信号。 (8)SIGALRM:定时器信号。 (9)SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。

    信号来源 信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:

    硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等。软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。

    信号生命周期和处理流程 (1)信号被某个进程产生,并设置此信号传递的对象(一般为对应进程的pid),然后传递给操作系统; (2)操作系统根据接收进程的设置(是否阻塞)而选择性的发送给接收者,如果接收者阻塞该信号(且该信号是可以阻塞的),操作系统将暂时保留该信号,而不传递,直到该进程解除了对此信号的阻塞(如果对应进程已经退出,则丢弃此信号),如果对应进程没有阻塞,操作系统将传递此信号。 (3)目的进程接收到此信号后,将根据当前进程对此信号设置的预处理方式,暂时终止当前代码的执行,保护上下文(主要包括临时寄存器数据,当前程序位置以及当前CPU的状态)、转而执行中断服务程序,执行完成后在回复到中断的位置。当然,对于抢占式内核,在中断返回时还将引发新的调度。

    信号的生命周期

    4 消息(Message)队列

    消息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。另外与管道不同的是,消息队列在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。延伸阅读:消息队列C语言的实践

    消息队列特点总结: (1)消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识. (2)消息队列允许一个或多个进程向它写入与读取消息. (3)管道和消息队列的通信数据都是先进先出的原则。 (4)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。 (5)消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。 (6)目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。

    5 共享内存(share memory)

    使得多个进程可以可以直接读写同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。进程就可以直接读写这一块内存而不需要进行数据的拷贝,从而大大提高效率。由于多个进程共享一段内存,因此需要依靠某种同步机制(如信号量)来达到进程间的同步及互斥。延伸阅读:Linux支持的主要三种共享内存方式:mmap()系统调用、Posix共享内存,以及System V共享内存实践

    共享内存原理图

    6 信号量(semaphore)

    信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。 为了获得共享资源,进程需要执行下列操作: (1)创建一个信号量:这要求调用者指定初始值,对于二值信号量来说,它通常是1,也可是0。 (2)等待一个信号量:该操作会测试这个信号量的值,如果小于0,就阻塞。也称为P操作。 (3)挂出一个信号量:该操作将信号量的值加1,也称为V操作。

    为了正确地实现信号量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。Linux环境中,有三种类型:Posix(可移植性操作系统接口)有名信号量(使用Posix IPC名字标识)、Posix基于内存的信号量(存放在共享内存区中)、System V信号量(在内核中维护)。这三种信号量都可用于进程间或线程间的同步。

    两个进程使用一个二值信号量

     

    两个进程所以用一个Posix有名二值信号量

    一个进程两个线程共享基于内存的信号量

    信号量与普通整型变量的区别: (1)信号量是非负整型变量,除了初始化之外,它只能通过两个标准原子操作:wait(semap) , signal(semap) ; 来进行访问; (2)操作也被成为PV原语(P来源于荷兰语proberen"测试",V来源于荷兰语verhogen"增加",P表示通过的意思,V表示释放的意思),而普通整型变量则可以在任何语句块中被访问;

    信号量与互斥量之间的区别: (1)互斥量用于线程的互斥,信号量用于线程的同步。这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。 在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源 (2)互斥量值只能为0/1,信号量值可以为非负整数。 也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。 (3)互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。

    7 套接字(socket)

    套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。

    Socket是应用层和传输层之间的桥梁

     

    套接字是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

    套接字特性 套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型。(1)套接字的域 它指定套接字通信中使用的网络介质,最常见的套接字域有两种:一是AF_INET,它指的是Internet网络。当客户使用套接字进行跨网络的连接时,它就需要用到服务器计算机的IP地址和端口来指定一台联网机器上的某个特定服务,所以在使用socket作为通信的终点,服务器应用程序必须在开始通信之前绑定一个端口,服务器在指定的端口等待客户的连接。另一个域AF_UNIX,表示UNIX文件系统,它就是文件输入/输出,而它的地址就是文件名。(2)套接字的端口号 每一个基于TCP/IP网络通讯的程序(进程)都被赋予了唯一的端口和端口号,端口是一个信息缓冲区,用于保留Socket中的输入/输出信息,端口号是一个16位无符号整数,范围是0-65535,以区别主机上的每一个程序(端口号就像房屋中的房间号),低于256的端口号保留给标准应用程序,比如pop3的端口号就是110,每一个套接字都组合进了IP地址、端口,这样形成的整体就可以区别每一个套接字。(3)套接字协议类型 因特网提供三种通信机制,一是流套接字,流套接字在域中通过TCP/IP连接实现,同时也是AF_UNIX中常用的套接字类型。流套接字提供的是一个有序、可靠、双向字节流的连接,因此发送的数据可以确保不会丢失、重复或乱序到达,而且它还有一定的出错后重新发送的机制。二个是数据报套接字,它不需要建立连接和维持一个连接,它们在域中通常是通过UDP/IP协议实现的。它对可以发送的数据的长度有限制,数据报作为一个单独的网络消息被传输,它可能会丢失、复制或错乱到达,UDP不是一个可靠的协议,但是它的速度比较高,因为它并一需要总是要建立和维持一个连接。三是原始套接字,原始套接字允许对较低层次的协议直接访问,比如IP、 ICMP协议,它常用于检验新的协议实现,或者访问现有服务中配置的新设备,因为RAW SOCKET可以自如地控制Windows下的多种协议,能够对网络底层的传输机制进行控制,所以可以应用原始套接字来操纵网络层和传输层应用。比如,我们可以通过RAW SOCKET来接收发向本机的ICMP、IGMP协议包,或者接收TCP/IP栈不能够处理的IP包,也可以用来发送一些自定包头或自定协议的IP包。网络监听技术很大程度上依赖于SOCKET_RAW。

    原始套接字与标准套接字的区别在于: 原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。

    套接字通信的建立

    Socket通信基本流程

    ** 服务器端** (1)首先服务器应用程序用系统调用socket来创建一个套接字,它是系统分配给该服务器进程的类似文件描述符的资源,它不能与其他的进程共享。 (2)然后,服务器进程会给套接字起个名字,我们使用系统调用bind来给套接字命名。然后服务器进程就开始等待客户连接到这个套接字。 (3)接下来,系统调用listen来创建一个队列并将其用于存放来自客户的进入连接。 (4)最后,服务器通过系统调用accept来接受客户的连接。它会创建一个与原有的命名套接不同的新套接字,这个套接字只用于与这个特定客户端进行通信,而命名套接字(即原先的套接字)则被保留下来继续处理来自其他客户的连接(建立客户端和服务端的用于通信的流,进行通信)。

    客户端 (1)客户应用程序首先调用socket来创建一个未命名的套接字,然后将服务器的命名套接字作为一个地址来调用connect与服务器建立连接。 (2)一旦连接建立,我们就可以像使用底层的文件描述符那样用套接字来实现双向数据的通信(通过流进行数据传输)。

    Processed: 0.012, SQL: 8