IPC-进程间通信

    技术2022-07-10  189

    一、进程间通信

    (IPC:Inter Process Communication) 1、概述 操作系统中的各个进程,通常存在于独立的内存空间,并且有着严格的机制来防止进程间的非法访问。但是,这并不代表进程与进程间不允许互相通信,相反,进程间通信是操作系统中一个重要的概念,应用非常广泛。 广义上,进程间通信是指运行在不同进程之间(不论是否在同一台机器)的若干线程间的数据交换。如下图所示: 原则上,任何跨进程的数据交换都可以称为进程间通信。IPC 中参与通信的进程,既可以运行在同一台机器上,也允许他们存在于各自的设备环境中(注:若进程是跨机器运行的,则通常由网络连接在一起)。 2、几种稳定高效的 IPC 机制 (1)共享内存(Shared Memory) 由于两个进程可以直接访问同一块内存区域,减少了数据的复制操作,因而速度上的优势比较明显。 实现内存共享的步骤: <1> 创建内存共享区 通过 API 从内存中,申请一块共享区域(例如:在Linux环境中,可通过 shmget() 函数来实现),生成的共享内存将与某个特定的 key(即:shmget() 函数中的第一个参数)进行绑定。 <2> 映射内存共享区 创建了共享内存区后,需要将其映射到进程1的空间中,才能进一步操作(例如:在Linux环境中,可通过 shmat() 函数来实现)。 <3> 访问内存共享区 内存共享区映射到进程1的空间后,进程2可以通过 shmget() 函数,并传入上面的key值,即可访问到。然后进程2执行 shmat() 函数,再将这块内存映射到自己的空间中。 <4> 进程间通信 共享内存在各个进程实现了内存映射后,便可以利用该区域进行信息的交换。由于内存共享本身并没有同步机制,所以参与通信的各个进程需要自己协商处理。 <5> 撤销内存映射区 完成了进程间通信后,各个进程都需要撤销之前的映射操作(例如:在Linux环境中,可通过 shmdt() 来实现)。 <6> 删除内存共享区 最后必须删除共享区域,以便回收内存(例如:在Linux环境中,可通过 shmctl() 函数来实现)。

    内存共享机制中相关函数说明: (2)管道(Pipe) 它适用于所有 POSIX 系统以及 Windows 系列产品,该种进程间通信方式的特点: <1> 进程A与进程B分立管道的两边,进行数据的传输通信。 <2> 管道中的流是单向的,意味着一个进程中若既需要”读“,也需要”写“,那么就要建立两根管道。 <3> 一根管道同时具有 “读取” 端(read end)、“写入” 端(write end)。 <4> 管道有容量限制,即:当Pipe满时,”读“、”写“操作将会被阻塞。

    在Linux环境中,使用Pipe的操作流程示例: 说明: <1> memset() 函数 该函数是一个初始化内存的 ”万能函数“,通常为新申请的内存进行初始化工作。它是直接操作内存空间的。该函数的原型为:

    # include <string.h> void *memset(void *s, int c, unsigned long n); // 其中,c参数一般使用0来进行初始化

    该函数的功能是:将指针变量 s 所指向的前n个字节的内存单元,用一个整型 c 替换,s 是 void* 型的指针变量,所以它可以为任何类型的数据进行初始化(注意:memset() 函数一般使用 ”0“ 来初始化内存空间,而且通常是用来给数组或结构体进行初始化)。 因此,memset() 函数就是在一段内存块中填充某个给定的值。因为它只能填充一个值,所以它无法将变量初始化为程序中需要的数据,初始化后,后面的程序再向该段内存空间中存放需要的数据。

    <2> fork() 函数 当一个进程调用 fork() 函数之后,就有两个二进制代码相同的进程,它们都运行到相同的地方,但每个进程都将开始自己的活动。 当控制转移到内核中的 fork() 函数代码后,内核开始做: 1.分配新的内存块和内核数据结构给子进程。 2.将父进程部分数据结构内容拷贝至子进程。 3.将子进程添加到系统进程列表。 4.fork() 函数返回开始调度器,进行调度。 子进程和父进程是并发运行的独立进程。内核能够以任意的方式交替执行他们在逻辑控制流中的指令。因为父进程和子进程是独立的进程,他们都有自己私有的地址空间,当父进程或者子进程单独改变时,不会影响到彼此。 fork() 函数的常规用法: 1.一个父进程希望复制自己,使得子进程同时执行不同的代码段,例如:父进程等待客户端请求时,可以生成一个子进程来等待请求处理。 2.一个进程要执行一个不同的程序时,可以生成子进程分别进行处理。

    <3> pipe() 函数 Linux 提供了Pipe接口,用于打开一个管道,该函数的原型为:

    int pipe(int pipefd[2], int flags); // 第一个参数代表成功打开后管道的两端

    (3)UDS(UNIX Domain Socket) 该机制是专门针对单机内的进程间通信提出来的,有时也被称为:IPC Socket。它与 Network Socket 的内部实现原理有很大区别,由于 UDS 是本机内安全可靠的操作,因此实现机制上不依赖 TCP/IP 等协议。 (注:Android 中使用最多的一种 IPC 机制是 Binder,其次是 UDS。Android 2.2版本之前使用 Binder 作为整个GUI架构中的 IPC基础,后来改用了 UDS。)

    使用 UDS 进行进程间通信的典型流程如下: <1> 服务器端监听 IPC 请求; <2> 客户端发起 IPC 申请; <3> 双方成功建立起 IPC 连接; <4> 客户端向服务器端发送数据,证明 IPC 通信是有效的。 UDS 的基本流程与 Network Socket 基本一致,只是在参数上有所区分,示例如下:

    // Server端源码 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/socket.h> // 引用的是socket头文件 #include <sys/un.h> #include <sys/types.h> #define UDS_PATH "uds_test" int main(void){ int socket_srv = -1; int socket_client = -1; int t = -1; int len = 0; struct sockaddr_un addr_srv, addr_client; // 注意地址格式与 Network Socket 的区别 char str[100]; // 用于接收通信数据 memset(str, 0, sizeof(char)*100); // 初始化数组 // 首先取得一个socket。UNIX系统支持 AF_INET, AF_UNIX, AF_NS 等几种domain类型。UDS 对应于AF_UNIX,而不是AF_INET if((socket_srv = socket(AF_UNIX, SOCK_STREAM, 0)) < 0){ return -1; } addr_srv.sun_family = AF_UNIX; strcpy(addr_srv.sun_path, UDS_PATH); // 设置 path_name // 将这个socket与地址进行绑定 if(bind(socket_srv, (struct sockaddr *)&addr_srv, offsetof(struct sockaddr_un, sun_path) + strlen(addr_srv.sun_path)) < 0){ return -1; } // 开始监听客户端的连接请求 if(listen(socket_srv, 10) < 0){ // 第二个参数表示支持的最大连接数 return -1; } while(1){ int nRecv; sz = sizeof(addr_client); // 需要特别注意,accept返回的socket和我们创建的不是同一个。addr_client是客户端的地址 if((socket_client = accept(socket_srv, (struct sockaddr *)&addr_client, &sz)) == -1){ return -1; } // 接收数据 if(nRecv = recv(socket_client, str, 100, 0) < 0){ close(socket_client); break; } // 将接收的数据返回客户端 if(send(socket_client, str, nRecv, 0) < 0){ close(socket_client); break; } // 传输完毕,关闭连接 close(socket_client); } return 0; } // 客户端源码 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/socket.h> // 引用的是socket头文件 #include <sys/un.h> #include <sys/types.h> #define UDS_PATH "uds_test" int main(void){ int sockey_client = -1; int data_len = 0; int addr_size = 0; struct sockaddr_un addr_srv; char str[100]; // 用于接收通信数据 memset(str, 0, sizeof(char)*100); // 初始化数组 strcpy(str, "This is a test for UDS!"); if((socket_client = socket(AF_UNIX, SOCK_STREAM, 0)) < 0){ return -1; } addr_srv.sun_family = AF_UNIX; strcpy(addr_srv.sun_path, UDS_PATH); // 设置 path_name addr_size = offsetof(struct sockaddr_un, sun_path) + strlen(addr_srv.sun_path); // Server 处于监听状态,client则通过这个函数与之建立连接 if(connect(socket_client, (struct sockaddr *)&addr_srv, addr_size) < 0){ return -1; } // 发送数据给服务器端进程 if(send(socket_client, str, strlen(str), 0) < 0){ close(socket_client); return -1; } // 接受服务器返回的数据 if(data_len = recv(socket_client, str, 100, 0) > 0){ str[data_len] = '\0'; printf("echo from server: %s", str); }else{ close(socket_client); return -1; } // 传输完毕,关闭连接 close(socket_client); return 0; }

    由上可知,建立一个 UDS 的过程相当繁琐,因此,UDS提供了一个便捷的函数,即:socketpair() ,它可以大大简化通信双方的工作。

    (4)RPC(Remote Procedure Calls:远程过程调用) 该机制涉及的通信双方通常运行于两台不同的机器中,在RPC机制中,开发人员不需要关注具体的中间传输过程是如何实现的。 一个完整的 RPC 通信,需要以下几个步骤: <1> 客户端进程调用 Stub 接口; <2> Stub 根据操作系统的要求进行打包,并执行相应的系统调用; <3> 由内核来完成与服务器端的具体交互,它负责将客户端的数据包发给服务器端的内核; <4> 服务器端 Stub 解包,并调用与数据包匹配的进程; <5> 进程执行操作; <6> 服务器实现以上步骤的逆向过程,将结果返回给客户端。

    二、Android 的进程和线程

    1、进程(Process)是程序的一个运行实例,以区别于 “程序” 这一静态的概念;线程(Thread)则是 CPU 调度的基本单位。

    2、Android 提供了特殊的方式,让不在同一个包里的组件,也可以运行于相同的进程中,分为两种情况: ( 优点:它们可以非常方便地进行资源共享,而不用经过进程间通信 ) (1)针对个别组件: 可以在 AndroidManfiest.xml 文件中的 <activity>、<service>、<receiver>、<provider> 标签中,加入 android:process 属性,来表明这一组件想要运行在哪个进程空间中。 (2)针对整个程序包: 可以在 AndroidManfiest.xml 文件中的 <application> 标签中,加入 android:process 属性,来指明想要依存的进程环境。

    3、对于 Android 应用,其四大组件(即:Activity、Service、BroadCast Recevicer、Content Provider)并不能算是完整的进程实例,只能算是进程的组成部分。应用程序启动后,将创建 ActivityThread 主线程。同一个包中的组件将运行在相同的进程空间中,不同包中的组件可以通过上述方式运行在一个进程空间中。一个 Activity 应用启动后,至少会有3个线程(即:1个主线程和2个Binder线程)。

    4、几个概念:Handler、MessageQueue、Runnable、Looper (1)Runnable 和 Message 可以被压入某个 MessageQueue 中,形成一个集合: (注意:一般情况下,某种类型的 MessageQueue 只允许保存相同类型的 Object。)

    (2)Looper:指的是循环地去做某件事。 (例如:上图中,它不断地从 MessageQueue 中,取出一个 item,然后传给 Handler 进行处理,如此循环往复,若 MessageQueue 为空,则它会进入休眠。)

    (3)Handler:是真正 “处理事情” 的地方,即:利用自身的处理机制,对传入的各种 Object 进行相应的处理,并产生最终的结果。 Handler类的两个主要作用: <1> 处理Message

    public void dispatchMessage(Message msg); // 对Message进行分发 public void handleMessage(Message msg); // 对Message进行处理

    Handler的扩展子类,可以通过重载 dispatchMessage() 和 handleMessage() 来改变它的默认行为方法。

    <2> 将某个Message压入MessageQueue中

    // 1.Post系列 final boolean post(Runnable r); final boolean postAtTime(Runnable r, long uptimeMills); // 2.Send系列 final boolean sendEmptyMessage(int what); final boolean sendMessageAtFrontOfQueue(Message msg); boolean sendMessageAtTime(Message msg, long uptimeMillis); final boolean sendMessageDelayed(Message msg, long delayMillis);

    说明:Post、Send两个系列函数方法的共同点是:都负责将某个 Message 压入 MessageQueue中;区别在于:Send系列函数的参数直接是Message,而Post系列函数则需要先把其它类型的 “零散” 信息转换为 Message,再调用Send系列函数来执行下一步。示例: 如上图所示:调用者提供的是 Runnable 对象,Post 需要将封装成一个 Message,当准备好Message后,程序调用对应的 Send 函数 (例如:sendMessageDelayed() 函数,该函数可以设定延迟多长时间后再发送消息,sendMessageAtTime() 函数内部又通过:当前时间+延迟时长,计算出具体是在哪个时间点发送消息),最后把它推送到 MessageQueue 中。 (注:之所以不直接调用 Handler进行处理,而是将其压入MessageQueue中随后再通过Handler进行处理,是因为 “有序性”。)

    (4)总结:Looper 不断获取 MessageQueue 中的一个 Message,然后由 Handler 进行处理,最终产生结果。 在 Android 系统中的实现如下图所示: 应用程序中使用 Looper 创建线程分如下两种情况: <1> 普通线程 示例:

    class LooperThread extends Thread{ public Handler mHandler; public void run(){ Looper.prepare(); // 1.Looper的准备工作(注:每个线程的Looper都是独立的) // 2.创建处理消息的handler对象 mHandler = new Handler(){ public void handleMessage(Message msg){ // 此处用于处理消息,继承Handler的子类,一般需要修改该方法 } }; // 3.Looper开始运作 Looper.loop(); // 进入主循环 } }

    <2> UI 主线程(Activity Thread) 示例:

    public static void main(String[] args){ ... Looper.prepareMainLooper(); // prepareMainLooper()内部也需要用到prepare() ActivityThread thread = new ActivityThread(); // 创建一个ActivityThread对象 thread.attach(false); // 参数false表示该线程不允许退出 if(sMainThreadHandler == null){ sMainThreadHandler = thread.getHandler(); // 主handler } AsyncTask.init(); Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited!"); }

    总结:下图表示一个进程内部的不同线程中Looper的情况,每个线程的方框表示它们所能访问的范围,例如:主线程就不能直接访问到普通线程中Looper对象,但二者都可以接触到进程的各个元素。 <1> 主线程使用的是 prepareMainLooper() 方法,该方法内部通过 prepare() 方法,为主线程生成一个 ThreadLocal 的 Looper 对象,并让sMainLooper 指向它(目的:若其它线程需要获取主线程的Looper,只需要调用 getMainLooper() 方法即可)。 <2> 普通线程使用的是 prepare() 方法,生成一个 ThreadLocal 的 Looper 对象,该对象只能在线程内通过 myLooper() 进行访问。

    三、Binder 机制

    1、Android 中的 Binder 机制 Binder 在 Android 系统中的地位非常高。在 Zygote 孵化出 system_server 进程后,在 system_server 进程中出初始化支持整个 Android framework 的各种各样的 Service,而这些 Service 从大的方向来划分,分为:Java Framework 层和 Native Framework 层(C++)的Service,几乎都是基于 Binder IPC 机制。 (1)Java framework层: 作为Server端继承(或间接继承)于 Binder 类,Client 端继承(或间接继承)于 BinderProxy 类。例如:ActivityManagerService(用于控制Activity、Service、进程等) 这个服务作为 Server 端,间接继承 Binder 类,而相应的 ActivityManager 作为 Client 端,间接继承于BinderProxy 类。 当然还有 PackageManagerService、WindowManagerService 等等很多系统服务都是采用 C/S 架构。 (2)Native Framework 层: 这是 C++ 层,作为 Server 端继承(或间接继承)于 BBinder 类,Client 端继承(或间接继承)于 BpBinder。例如:MediaPlayService(用于多媒体相关)作为 Server 端,继承于 BBinder 类,而相应的 MediaPlay 作为 Client 端,间接继承于 BpBinder 类。

    2、Binder 机制的架构:

    Processed: 0.031, SQL: 9