进程间通信(IPC)

进程间通信指的是系统中两个进程之间的通信,不同的进程都在各自的地址空间中、相互独立、隔
离,所以它们是处在于不同的地址空间中,因此相互通信比较难, Linux内核提供了多种进程间通信的机制。
Linux中IPC通信机制有:
(1)管道
(2)FIFO
(3)信号
(4)信号量
(5)消息队列
(6)共享队列
(7)Socket IPC:基于Socket进程间通信

管道和 FIFO
管道包括三种:
a、 普通管道pipe:通常有两种限制,一是单工,数据只能单向传输;二是只能在父子或者兄弟进程间
使用;
b、 流管道s_pipe:去除了普通管道的第一种限制,为半双工,可以双向传输;只能在父子或兄弟进程
间使用;
c、 有名管道name_pipe(FIFO):去除了普通管道的第二种限制,并且允许在不相关(不是父子或兄
弟关系)的进程间进行通讯。
普通管道可用于具有亲缘关系的进程间通信,并且数据只能单向传输,如果要实现双向传输,则必须要
使用两个管道;而流管道去除了普通管道的第一种限制,可以半双工的方式实现双向传输,但也只能在具有亲缘关系的进程间通信;而有名管道(FIFO)则同时突破了普通管道的两种限制,即可实现双向传输、又能在非亲缘关系的进程间通信。

管道的分类和使用
Linux上的管道分两种类型:

匿名管道
命名管道

这两种管道也叫做有名或无名管道。匿名管道最常见的形态就是我们在shell操作中最常用的”|”。它的特点是只能在父子进程中使用,父进程在产生子进程前必须打开一个管道文件,然后fork产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。此时除了父子进程外,没人知道这个管道文件的描述符,所以通过这个管道中的信息无法传递给其他进程。这保证了传输数据的安全性,当然也降低了管道了通用性,于是系统还提供了命名管道。

我们可以使用mkfifo或mknod命令来创建一个命名管道,这跟创建一个文件没有什么区别:

Linux系统无论对于命名管道和匿名管道,底层都用的是同一种文件系统的操作行为,这种文件系统叫pipefs。

PIPE
我们可以把匿名管道和命名管道分别叫做PIPE和FIFO。这主要因为在系统编程中,创建匿名管道的系统调用是pipe(),而创建命名管道的函数是mkfifo()。使用mknod()系统调用并指定文件类型为为S_IFIFO也可以创建一个FIFO。

使用pipe()系统调用可以创建一个匿名管道,这个系统调用的原型为:

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

这个方法将会创建出两个文件描述符,可以使用pipefd这个数组来引用这两个描述符进行文件操作。pipefd[0]是读方式打开,作为管道的读描述符。pipefd[1]是写方式打开,作为管道的写描述符。从管道写端写入的数据会被内核缓存直到有人从另一端读取为止。

FIFO
命名管道在底层的实现跟匿名管道完全一致,区别只是命名管道会有一个全局可见的文件名以供别人open打开使用。再程序中创建一个命名管道文件的方法有两种,一种是使用mkfifo函数。另一种是使用mknod系统调用,例子如下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>int main(int argc, char *argv[])
{if (argc != 2) {
        fprintf(stderr, "Argument error!\n");
        exit(1);
    }/*
    if (mkfifo(argv[1], 0600) < 0) {
        perror("mkfifo()");
        exit(1);
    }
*/
    if (mknod(argv[1], 0600|S_IFIFO, 0) < 0) {
        perror("mknod()");
        exit(1);
    }exit(0);
}

我们使用第一个参数作为创建的文件路径。创建完之后,其他进程就可以使用open()、read()、write()标准文件操作等方法进行使用了。其余所有的操作跟匿名管道使用类似。需要注意的是,无论命名还是匿名管道,它的文件描述都没有偏移量的概念,所以不能用lseek进行偏移量调整。

信号
信号是事件发生时对进程的通知机制,也可以把它称为软件中断。信号与硬件中断的相似之处在于能够
打断程序当前执行的正常流程,其实是在软件层次上对中断机制的一种模拟。大多数情况下,是无法预测信号达到的准确时间,所以,信号提供了一种处理异步事件的方法。 Linux信号总结如下:
在这里插入图片描述
Tips:上表中,term 表示终止进程;core 表示生成核心转储文件,核心转储文件可用于调试,这个便不
再给介绍了;ignore表示忽略信号;cont表示继续运行进程;stop 表示停止进程(注意停止不等于终止,而是暂停)。

进程对信号的处理
当进程接收到内核或用户发送过来的信号之后,根据具体信号可以采取不同的处理方式:忽略信号、捕
获信号或者执行系统默认操作。Linux系统提供了系统调用 signal()和 sigaction()两个函数用于设置信号的处理方式。

signal()函数
描述系统调用 signal(),signal()函数是 Linux 系统下设置信号处理方式最简单的接口,可将信号的
处理方式设置为捕获信号、忽略信号以及系统默认操作,此函数原型如下所示:

#include <signal.h> 
typedef void (*sig_t)(int); 
sig_t signal(int signum, sig_t handler); 

使用该函数需要包含头文件<signal.h>。

sigaction()函数
除了signal()之外, sigaction()系统调用是设置信号处理方式的另一选择, 事实上,推荐大家使用sigaction()函数。虽然 signal()函数简单好用,而 sigaction()更为复杂,但作为回报,sigaction()也更具灵活性以及移植性。
sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时
的行为施以更加精准的控制,其函数原型如下所示:

#include <signal.h> 
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 

使用该函数需要包含头文件<signal.h>。

信号量
信号量是一个计数器, 与其它进程间通信方式不大相同, 它主要用于控制多个进程间或一个进程内的多
个线程间对共享资源的访问,相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志,除了用于共享资源的访问控制外,还可用于进程同步。
它常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源,因此,主要作为进程间以及同
一个进程内不同线程之间的同步手段。
semaphore数据结构的定义如下。

<include/linux/semaphore.h>
struct semaphore{
				raw_spinlock_t     lock;
				unsigned int       count;
				struct list_head   wait_list;
}

消息队列
消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少、管
道只能承载无格式字节流以及缓冲区大小受限等缺陷。消息队列包括 POSIX 消息队列和 System V 消息队列。
对于系统中的每个消息队列,内核维护一个定</include/uapi/linux/msg.h>头文件中的信息结构。

/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct msqid_ds {
	struct ipc_perm msg_perm;
	struct msg *msg_first;		/* first message on queue,unused  */
	struct msg *msg_last;		/* last message in queue,unused */
	__kernel_old_time_t msg_stime;	/* last msgsnd time */
	__kernel_old_time_t msg_rtime;	/* last msgrcv time */
	__kernel_old_time_t msg_ctime;	/* last change time */
	unsigned long  msg_lcbytes;	/* Reuse junk fields for 32 bit */
	unsigned long  msg_lqbytes;	/* ditto */
	unsigned short msg_cbytes;	/* current number of bytes on queue */
	unsigned short msg_qnum;	/* number of messages in queue */
	unsigned short msg_qbytes;	/* max number of bytes on queue */
	__kernel_ipc_pid_t msg_lspid;	/* pid of last msgsnd */
	__kernel_ipc_pid_t msg_lrpid;	/* last receive pid */
};

msgget函数
msgget函数用于创建新的消息队列或者访问一个已存在的消息队列。

#include<sys/msg.h>
int msgget(key_t key, int oflag)
返回:若成功则为非负标识符,若出错则为-1

msgsnd函数
使用msgget打开一个消息队列后,我们使用msgsnd往其上放置一个消息。

#include<sys/msg.h>
int msgsnd(int msqid, const void* ptr, size_t length,  int flag)
返回:若成功则为0,若出错则为-1

msgrcv函数
使用msgrcv函数从某个消息队列中读出一个消息。

#include<sys/msg.h>
ssize_t msgrcv (int msqid, void* ptr, size_t length, long type, int flag)
返回:若成功则为读入缓冲区中数据的字节数,若出错则为-1

msgctl函数
msgctl函数提供在一个消息队列上的各种控制操作。

#include<sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds* buff);
返回:若成功则为0,若出错则为-1

共享内存
管道、FIFO和消息队列的问题在于,两个进程要交换信息时,这些信息必须经由内核传递。
共享内存就是映射一段能被其它进程所访问的内存, 这段共享内存由一个进程创建, 但其它的多个进程都可以访问,使得多个进程可以访问同一块内存空间,无需内核参与。共享内存是最快的 IPC 方式,它是针对其它进程间通信方式运行效率低而专门设计的,它往往与其它通信机制,譬如结合信号量来使用,以实现进程间的同步和通信。
对于每个共享内存区,内核维护如下的信息结构,定义在/include/uapi/linux/shm.h头文件中,

/include/uapi/linux/shm.h
/* Obsolete, used only for backwards compatibility and libc5 compiles */
struct shmid_ds {
	struct ipc_perm		shm_perm;	/* operation perms */
	int			shm_segsz;	/* size of segment (bytes) */
	__kernel_old_time_t	shm_atime;	/* last attach time */
	__kernel_old_time_t	shm_dtime;	/* last detach time */
	__kernel_old_time_t	shm_ctime;	/* last change time */
	__kernel_ipc_pid_t	shm_cpid;	/* pid of creator */
	__kernel_ipc_pid_t	shm_lpid;	/* pid of last operator */
	unsigned short		shm_nattch;	/* no. of current attaches */
	unsigned short 		shm_unused;	/* compatibility */
	void 			*shm_unused2;	/* ditto - used by DIPC */
	void			*shm_unused3;	/* unused */
};

shmget函数
shmget函数创建一个新的共享内存区,或者访问一个已存在的共享内存区

#include <sys/shm.h>
int shmget(key_t key, size_t size, ing oflag);
返回:若成功则为共享内存区对象,若出错则为-1

shmat函数
由shmget创建或打开一个共享内存后,通过调用shmat把它附接到调用进程的地址空间。

#include <sys/shm.h>
int *shmat(int shmid, const void* shmaddr, ing flag);
返回:若成功则为映射区的起始地址,若出错则为-1

shmdt函数
当一个进程完成某个共享内存区的使用时,它可调用shmdt断接这个内存区域

#include <sys/shm.h>
int *shmdt(const void* shmaddr);
返回:若成功则为0,若出错则为-1

shmctl函数
shmctl提供了对一个共享内存区的多种操作

#include <sys/shm.h>
int *shmctl(int shmid, int cmd, struct shmid_ds* buff);
返回:若成功则为0,若出错则为-1

DBUS

D-Bus是Linux及其他类UNIX系统上的一种IPC(Interprocess communication)机制。相较于传统的管道(PIPE)、Socket等原生基于字节流的IPC方式,D-Bus提供了基于独立Message的传输方式,应用程序使用起来更加简单。D-Bus的设计初衷是为Linux桌面环境上的一系列应用程序提供通信方式,它的设计里保留了许多对上层框架设计考虑的元素。

D-Bus的常用架构与传统的Socket一对一通信模式不同,它基于中间消息路由转发的模式来实现, 如下图:
在这里插入图片描述
D-Bus默认提供两种BUS,系统BUS(system)和会话BUS(session)。系统BUS在每台机器上是惟一的,用于后台服务及操作系统之间的通信。会话BUS用于每个登录用户会话的应用程序之间的通信。每个BUS实例由一个bus-daemon进程来管理,由其负责消息路由转发。应用程序需要收发消息,需要连接到BUS实例上。BUS实例使用基于XML的配置文件来控制安全策略,如用户能否注册服务,能给哪些服务接口发送消息等等。

下面介绍D-Bus的一些概念。

Service
也叫BUS Name,这个名称有些令人费解。它并不是指BUS实例的名称,而应该理解为进程连接到BUS实例之后的名称。BUS实例依据BUS Name转发消息

object
一个D-Bus服务可以包含多个对象, 每个对象对外提供一组功能特性。它是一个独立处理消息的实体,也就是说消息的传送路径为对象到对象。不同的编程语言和库中,定义了自身领域内的对象,如JAVA中的java.lang.Object, GLIB中的GObject, QT中的QObject等。D-Bus使用通用的对象路径的方式来让更高层的接口来绑定到特定语言和库的对象中。这种对象路径类似于类UNIX系统中的文件路径,如/org/freedesktop/systemd。这种路径只是提供了对象的惟一标识符,不同的服务实现可以自行来决定是否需要使用其层次性的价值。

Interface
object支持一个或者多个接口,接口是一组方法和信号的集和。可以理解为方法(methods)和信号(signals)的命名空间。命名也是采用反向域名方式,如org.freedesktop.DBus。大多数编程语言的D-Bus绑定库会将这些接口映射为语言本身的结构,如JAVA中的Interface,C++的纯虚类。

Members
Interface中成员可以包含多个methods和signals。Methods表示能够被其他进程调用的操作,可以带有参数和响应结果。Signals是一种带有Payload的广播消息,进程可以定义自己感兴趣的Signals。这两种情形分别对应RPC(Remote Procedure Call)和Publish/Subscribe模式。

D-Bus的消息中会包含以上的这些信息,如下图所示:
在这里插入图片描述

实际上,这些面向对象的结构设计更多地是为更高层的语言绑定服务。若我们直接使用底层的libdbus库来开发程序,可以简单地将其理解为消息内容的不同区分标识。
值得注意的是,D-Bus本身实现性能上并不是特别高,因而只适合于传送控制消息而非数据。并且现在用于实现RPC和PUB/SUB场景的中间件选择非常多,如RabbitMQ、Redis等, 使用起来都更为简单。在一般情形下更推荐使用这些消息队列类型的中间件。但是若需要和系统本身的一些使用D-Bus的服务(如systemd, NetworkManager)需要交互时,使用D-Bus则是一个非常方便的方式。

Binder
Android系统中用于进程间通信

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值