Wayland中的跨进程过程调用浅析

原文地址:http://blog.csdn.net/jinzhuojun/article/details/40264449

Wayland协议主要提供了Client端应用与Server端Compositor的通信机制,Weston是Server端Compositor的一个参考实现。Wayland协议中最基础的是提供了一种面向对象的跨进程过程调用的功能,在作用上类似于Android中的Binder。与Binder不同的是,在Wayland中Client和Server底层通过domain socket进行连接。和Binder一样,domain socket支持在进程间传递fd,这为传递graphic buffer和shared memory等提供了基础。Client和Server端一方面各自在消息循环中等待socket上的数据,拿到数据后经过反序列化处理生成本地的函数调用,然后进行调用;另一方面将本地的远端调用请求封装序列化后通过socket发出。另外,由于是基于消息循环的处理模型,意味着这种调用不是同步,但通过等待Server端处理完成后的事件再返回可以达到同步调用的效果。

下面从几个基本方面介绍Wayland协议的跨进程过程调用是如何实现的。

一、基本工作流程

以Weston自带的例程simple-shm为例,先感受一下Client如何通过Wayland协议和Compositor通信。

  • 1.连接Server,绑定服务

  • 2.创建窗口

为什么一个窗口要创建两个surface呢?因为Wayland协议假设Server端对Surface的管理分两个层次。以Weston为例,Compositor只负责合成(代码主要在compositor.c),它相当于Android中的SurfaceFligner,它所看到的主要是weston_surface。而Weston在启动时会加载shell模块(如desktop-shell.so,代码主要在desktop-shell/shell.c),它相当于Android中的WindowManagerService,它所看到的主要是shell_surface。shell_surface在结构上是weston_surface的进一步封装,为了做窗口管理。这样,合成渲染和窗口管理的模块既可以方便地相互访问又保证了较低的耦合度。

  • 3.分配buffer与绘制

与Android作个类比,这里的wl_surface对应SF中的Layer,wl_buffer对应GraphicBuffer。Weston对应SF+WMS。一个surface对应用户视角看到的一个窗口。为了使Client和Server并行作业,一般会用多个buffer。和Android比较不同的是,Android是Server端来维护buffer的生命周期,而Wayland中是Client端来做的。

二、连接的建立

首先Server端的Compositor在启动时会在$XDG_RUNTIME_DIR目录下创建用于监听的socket,默认为wayland-0。然后把该socket fd加入event loop等待的fd列表。参考实现位于weston_create_listening_socket() -> wl_display_add_socket_auto() -> _wl_display_add_socket()。当有Client连接时,回调处理函数为socket_data(),其中会调用wl_os_accept_cloexec()得到与该Client相连的socket fd。然后调用wl_client_create(),创建wl_client。Server端会为每一个连接的Client创建wl_client,这些对象被串在display->client_list这个列表里。wl_client中的wl_connection代表这个与Client的连接,其中包含了in buffer和out buffer,分别作为输入和输出的缓冲区。注意这个in buffer和out buffer都是双份的,一份for普通数据,一份for fd,因为fd需要以out-of-band形式传输,要特殊处理。wl_event_loop_add_fd()会把这个与Client连接的socket fd加入到event loop等待的列表中,回调处理函数为wl_client_connection_data()。

当有Client连接到Server的监听socket上,会调用刚才注册的回调函数socket_data(),然后会调用wl_os_accept_cloexec()->accept()创建与Client连接的fd。接着调用wl_client_create()创建Client对象。

Client端要连接Server端,是通过调用wl_display_connect()。其中会创建socket并且调用connect()连接Server端创建的监听端口。得到与Server端连接的socket fd后调用wl_display_connect_to_fd()创建
wl_display。wl_display是Server端的display资源在Client端的代理对象,它的第一个元素wl_proxy,因此它可以与wl_proxy互转。和Server端一样,也会创建一个wl_connection包含读写缓冲区。

可以看到display在Client端的wl_proxy和Server端wl_resource都包含了它完整的接口描述wl_display_interface。但wl_proxy只包含了event的实现display_listener,wl_resource只包含了request的实现display_interface。

三、消息处理模型

在Server端,Compositor初始化完后会调用wl_display_run()进入大循环。这个函数主体是:

wl_event_loop代表主消息循环,wl_event_loop_dispatch()的大多数时间会通过epoll_wait()等待在wl_event_loop的epoll_fd上。epoll是类似于select, poll的机制,可以让调用者等待在一坨fd上,直到其中有fd可读、可写或错误。这个event loop和其中的epoll_fd是Compositor在wl_display_create() -> wl_event_loop_create()时创建的。

wl_event_source代表wl_event_loop中等待的事件源。它有多种类型,比如wl_event_source_fd, wl_event_source_timer和wl_event_source_signal。它们分别代表由socket fd, timerfd和signalfd触发的事件源。wl_event_source_idle比较特殊,当消息循环处理完那些epoll_fd上等到的事件后,在下一次阻塞在epoll_wait()上前,会先处理这个idle list里的事件。比如有Client更新graphic buffer后会调用weston_output_schedule_repaint() -> wl_event_loop_add_idle(),就是往这个idle list里加一个消息。wl_event_source_fd的创建为例,在Client连接到Server时,Server会调用wl_client_create() -> wl_event_loop_add_fd()->add_source()将之加入到display的loop上,其处理回调函数为wl_client_connection_data(),意味着当主消息循环在这个Client对应的socket上读到数据时,就会调用wl_client_connection_data()进行处理。

在Client端,当需要与Server端交换数据时,最终会调用wl_display_dispatch_queue()。其中最主要的是三件事:

  • 1.wl_connection_flush()将当前out buffer中的数据通过socket发往Server端。这些数据是之前在wl_connection_write()中写入的。
  • 2.通过poll()在socket上等待数据,并通过read_events()将这些数据处理生成函数闭包结构wl_closure,然后放到display的wl_event_queue.event_list事件列表中。wl_closure可以看作是一个函数调用实例,里面包含了一个函数调用需要的所有信息。
  • 3.dispatch_queue()->dispatch_event()用于处理前面添加到队列的事件。这里就是把队列中的wl_closure拿出来生成trampoline后进行调用。

四、跨进程过程调用

术语上,Wayland中把Client发给Server的跨进程函数调用称为request,反方向的跨进程函数调用称为event。本质上,它们处理的方式是类似的。要让两个进程通过socket进行函数调用,首先需要将调用抽象成数据流的形式。函数的接口部分是同时链接到Client和Server端的库中的,其中包含了对象所支持的request和event的函数签名。因此这部分不用传输,只要传输目标对象id,方法id和参数列表这些信息就可以了。这些信息会通过wl_closure_marshal()写入wl_closure结构,再由serialize_closure()变成数据流。等到了目标进程后,会从数据流通过wl_connection_demarshal()转回wl_closure。这个过程类似于Android中的Parcel机制。那么问题来了,参数中的整形,字符串什么的都好搞,拷贝就行。但如果参数中包含对象,我们不能把整个对象拷贝过去,也不能传引用过去。那么需要一种机制来作同一对象在Server和Client端的映射,这是通过wl_map实现的。wl_map在Client和Server端各有一个,它们分别存了wl_proxy和wl_resource的数组,且是一一对应的。这些对象在这个数组中的索引作为它们的id。这样,参数中的对象只要传id,这个id被传到目的地后会通过查找这个wl_map表来得到本地相应的对象。在功能上类似于Android中的BpXXX和BnXXX。wl_proxy和wl_resource都包含wl_object对象。这个wl_object和面向对象语言里的对象概念类似,它有interface成员描述了这个对象所实现的接口,implementation是这些接口的实现函数的函数指针数组,id就是在wl_map结构里数组中的索引。前面所说的Client绑定Server端资源的过程就是在Client端创建wl_proxy,在Server端创建wl_resource。然后Client就可以通过wl_proxy调用Server端对应wl_resource的request,Server端就可以通过wl_resource调用Client端对应wl_proxy的event。这个映射过程如下图所示(以wl_registry为例):

wayland-1

与Android不同的是,Android中请求到达Server端,调用时需要在目标对象中有一段Stub来生成调用的上下文。而Wayland中,这是由libffi完成的。

Wayland核心协议是通过protocol/wayland.xml这个文件定义的。它通过wayland_scanner这个程序扫描后会生成wayland-protocol.c, wayland-client-protocol.h和wayland-server-protocol.h三个文件。wayland-client-protocol.h是给Client用的;wayland-server-protocol.h是给Server用的; wayland-protocol.c描述了接口,Client和Server都会用。这里以wl_display的get_registry()这个request为例,分析下跨进程的过程调用是如何实现的。

首先在wayland.xml中申明wl_display有get_registry这个request:

这里的参数类型是new_id,说明需要新建一个代理对象。其它的如object代表一个对象,fd代表代表文件描述符等。

wayland-protocol.c中描述了wl_display这个对象的request和event信息,其中包含了它们的函数签名。get_registry是request中的一项。

wayland-server-protocol.h中:

这个声明是让Server端定义implementation中的实现函数列表用的,如:

wayland-client-protocol.h中:

这是给Client端用来发起request的。当客户端调用wl_display_get_registry(),由于要返回代理对象,所以调用wl_proxy_mashal_constructor()。返回的wl_registry是一个代理对象。

到了Server端,前面提到会调用处理函数wl_client_connection_data()进行处理:

在这个场景下,由于之前在bind_display()中把client->display_resource的implementation设为:

所以接下来会调用到display_get_registry()。这个函数里会创建wl_registry对应的wl_resource对象。创建好后会放到display->registry_resource_list中。前面提到过,这个registry资源逻辑上的作用是Client放在Server端的Observer,它用于监听Server端有哪些Global对象(Service服务)。display_get_registry()函数接下去会对于每一个Global对象向该Client新建的registry发送事件。另外在有Global对象创建和销毁时(wl_global_create()和wl_global_destroy()),Server会向所有的registry发送事件进行通知。因此,Global对象可以理解为可动态加载的Service。

那么,这些Global对象具体都是些什么呢?为了故事的完整性,这里就插播段题外话。Server端的Compositor在启动时一般会注册一些Global对象,逻辑上其实就是一些服务。通过Wayland提供的wl_global_create()添加:

以wl_compositor这个Global对象为例, Server端调用wl_global_create(display, &wl_compositor_interface, 3, ec, compositor_bind)。然后当Client端调用wl_display_get_registry()时,Server端的display_get_registry()会对每个Global对象向Client发送global事件,因此Server端有几个Global对象就会发几个event。Client收到event后调用先前注册的回调registry_handle_global()。根据interface name判断当前发来的是哪一个,然后调用wl_reigistry_bind(…, &wl_compositor_interface,..)绑定资源,同时创建本地代理对象。接着Server端相应地调用registry_bind(),其中会调用先前在wl_global_create()中注册的回调函数,即compositor_bind()。接着经过wl_resource_create(), wl_resource_set_implementation()等创建wl_resource对象。也就是说,对于同一个Global对象,每有Client绑定一次,就会创建一个wl_resource对象。换句话说,对于Server来说,每一个Client有一个命名空间,同一个Global对象在每一个Client命名空间的wl_resource是不一样的。这样,对于一个Global对象(Service服务),在Client端创建了wl_proxy,在Server端创建了wl_resource,它们就这样绑定起来了。wl_proxy.object包含了event的处理函数,这是对Server端暴露的接口,而wl_resource.object包含了request的处理函数,这是对Client暴露的接口。

回到故事主线上,前面是从Client端调用Server端对象的request的流程,从Server端向Client端对象发送event并调用其回调函数的过程也是类似的。下面以display_get_registry()中向Client端发送global事件为例分析下流程。Server端通过wl_resource_post_event()来向Client发送event。

这样event就被放到connection的out buffer中,等待从socket上发送。那么,Client是怎么读取和处理这些event呢?首先Client端需要监听这个wl_proxy,这是通过调用wl_registry_add_listener()->wl_proxy_add_listener()设置的。该函数的参数中包含了这个event的处理函数列表registry_listener,它对应的接口在前面调用wl_display_get_registry()时已设置成wl_registry_interface。wl_registry_interface是在前面根据wayland.xml自动生成的一部分。这里体现了event与request的一点细微差别,request是Server端都要处理的,而event在Client可以选择不监听。

然后在Client的主循环中会调用wl_display_dispatch_queue()来处理收到的event和发出out buffer中的request:

这样该event的相应处理回调函数就被调用了,在这个场景中,即registry_handle_global()。下图简单地描绘了整个流程。

wayland-2

0 条评论
发表一条评论

注意: 评论者允许使用'@user空格'的方式将自己的评论通知另外评论者。例如, ABC是本文的评论者之一,则使用'@ABC '(不包括单引号)将会自动将您的评论发送给ABC。使用'@all ',将会将评论发送给之前所有其它评论者。请务必注意user必须和评论者名相匹配(大小写一致)。