2020-11-25 wayland 進程間函數調用

桌面環境做了好幾年,對 X 和 wayland 比較感興趣,但是一直沒有機會深入去看。正好我要離職了,在現在這個單位,北京上海加起來有將近十年了,最年輕的十年,也收獲了不少。最近工作交接完沒啥事,就抓著 wayland 代碼看了看,整體概念大概了解一點,先總結一下吧,免得又忘了。這篇文章主要目的是梳理 server 端和 client 端的函數調用是怎么實現的?并且拿 display 和 registry 為例做簡單地說明。

wayland 核心協議定義了許多全局對象,包括:display、registry、compositor、output、seat、surface、buffer 等等。客戶端連接到服務端后,會創建一個客戶端的可用對象,一般叫 proxy,服務端對應會創建一個 resource,這倆是對應的,實現 client 端對 server 端的資源綁定。

client 和 server 一方面各自在消息循環上等待數據(socket 上的數據),拿到數據經過反序列化后生成本地的函數掉用;另一方面,將本地的調用請求封裝序列化后,通過 socket 發出去。

wayland 協議

核心協議通過 xml 文件來定義,wayland-scanner 程序掃描并生成一個 wayland-protocol.c 和兩個頭文件,分別給服務端程序和客戶端程序使用。
以 display 和 registry 為例,在生成的 wayland-protocol.c 中,display 的簽名:

static const struct wl_message wl_display_requests[] = {
    { "sync", "n", wayland_types + 8 }, // wl_callback_interface
    { "get_registry", "n", wayland_types + 9 }, //wl_registry_interface
};

static const struct wl_message wl_display_events[] = {
    { "error", "ous", wayland_types + 0 },
    { "delete_id", "u", wayland_types + 0 },
};

WL_EXPORT const struct wl_interface wl_display_interface = {
    "wl_display", 1,
    2, wl_display_requests,
    2, wl_display_events,
};

wl_message 的結構:

 struct wl_message {
    /** Message name */
    const char *name;
    /** Message signature */
    const char *signature;
    /** Object argument interfaces */
    const struct wl_interface **types;
};

{ "get_registry", "n", wayland_types + 9 } 的這個 n 表示 new_id,也就是說客戶端在發出 get_registry 請求的時候會傳遞一個 wl_interface * 的參數,對應為這個參數創建一個新的對象。比如 wl_display_get_registry 返回 wl_registry 對象,這個對象就是 registry 在客戶端的代理對象,這樣創建出來的。registry 對象的相關簽名如下:

static const struct wl_message wl_registry_requests[] = {
    { "bind", "usun", wayland_types + 0 },
};

static const struct wl_message wl_registry_events[] = {
    { "global", "usu", wayland_types + 0 },
    { "global_remove", "u", wayland_types + 0 },
};

WL_EXPORT const struct wl_interface wl_registry_interface = {
    "wl_registry", 1,
    1, wl_registry_requests,
    2, wl_registry_events,
};

簽名類型簡單羅列如下:

 * * `i`: int
 * * `u`: uint
 * * `f`: fixed
 * * `s`: string
 * * `o`: object
 * * `n`: new_id
 * * `a`: array
 * * `h`: fd
 * * `?`: 問號表示后面接的參數可空

wl_message 有個成員 types,在 demarshal 消息的過程中,如果傳過來的參數中有 object 或者 new_id,那么我們設置的 implementation 必須要知道對象的類型是什么,這個 types 就是用來做這個的。

wayland_types 是個指針數組,數組里面的成員是 wl_interface *,比如 global 對應 wayland_types + 0 指向 NULL,get_registry 對應的 wayland_types + 9 指向 wl_registry_interface。
對于 display,客戶端先創建一個 proxy,將 request 發給服務端,服務端收到消息后創建對應的 display_resource,這個 display_resource 與客戶端的 display_proxy 是對應的,客戶端給服務端發送 request 消息,服務端給客戶端發送 event 消息。display 的 request 有兩個: sync 和 get_registry,在服務端實現,event 有兩個: error 和 delete_id,在客戶端實現。

服務端

1、wayland server 啟動時會創建一個 socket(_wl_display_add_socket),并將這個 socket fd 加入 epoll 中,這樣如果有客戶端程序連接,epoll 就會通知服務端,從而執行回調函數 socket_data。

2、當有客戶端程序連接過來,在 socket_data 函數中調用 accept 創建新的 socket fd,緊接著創建 wl_client,并把它加入合成器的 client list;在創建 wl_client 的時候,wl_client_create (display, fd) 將這個 socket fd 加入 epoll,這樣從客戶端發過來的請求,也可以通過 epoll 通知服務端,回調函數是 wl_client_connect_data。

3、等創建了 wl_client,接著執行函數 bind_display,創建 wl_display 在 server 端的 resource 對象 client->display_resource,設置它的 implementation,也就是 wl_display_requests 的倆函數:

static const struct wl_display_interface display_interface = {
    display_sync,
    display_get_registry
};

客戶端

1、客戶端程序在運行時先調用 wl_display_connect 連接到服務端,這個函數返回的 wl_display 就是 display 對象在 client 端的代理[上面第 3 步中說了 server 端是 display_resource,這倆配合工作,實現互相調用函數]。
在 wl_display_connect 中為代理對象 set_implementation,也就是 wl_display_events

static const struct wl_display_listener display_listener = {
    display_handle_error,
    display_handle_delete_id
};

這時第一個資源對象 display 就在 server 和client 端都設置好了。

2、然后調用 wl_display_get_registry 來獲取 server 端所有可用的 global 對象,并執行 wl_registry_add_listener 為新的 registry 對象注冊事件:

static const struct wl_registry_listener registry_listener = {
    registry_handle_global,
    registry_handle_global_remove
};

函數 wl_display_get_registry,它會調用下面這個函數
wl_proxy_marshal_constructor((struct wl_proxy *) wl_display, WL_DISPLAY_GET_REGISTRY, &wl_registry_interface, NULL);
上面的 &wl_registry_interface 就是 get_registry 的簽名 "n"。一方面它會返回 registry 在 客戶端的 proxy,也就是 display->registry,另一方面把請求發出去。

簡單分析這兩個函數,首先介紹
wl_registry_add_listener(display->registry, &registry_listener, display);

第一個參數是 registry 的客戶端 proxy,第二個參數是 implementation,這個函數的作用就是設置 proxy event 的 implementation

    proxy->object.implementation = implementation;
    proxy->user_data = data;

接著再分析 wl_proxy_marshal_constructor,在 wayland-client.c 中層層調用到了 wl_proxy_marshal_array_constructor_versioned:

    message = &proxy->object.interface->methods[opcode];
    if (interface) {
        new_proxy = create_outgoing_proxy(proxy, message,
                          args, interface,
                          version);
        if (new_proxy == NULL)
            goto err_unlock;
    }

解析 message 這里需要先看 wl_interface 的結構定義,如下:

struct wl_interface {
    /** Interface name */
    const char *name;
    /** Interface version */
    int version;
    /** Number of methods (requests) */
    int method_count;
    /** Method (request) signatures */
    const struct wl_message *methods;
    /** Number of events */
    int event_count;
    /** Event signatures */
    const struct wl_message *events;
};

其中 methods 是 request 的簽名,這里的 object.interface 應該是 &wl_display_interface,在函數 wl_display_connect 設置它的值:
display->proxy.object.interface = &wl_display_interface;
這個接口的 methods 有兩個,method_count 就是2; opcode 是 WL_DISPLAY_GET_REGISTRY,拿到的 message 也就是 “{ "get_registry", "n", wayland_types + 9 }”。
傳遞過來的 interface 為 wl_registry_interface,在 create_outgoing_proxy 得到 “n”,就會創建新的 proxy 對象,設置這個對象的 interface 為 wl_registry_interface,并把這個新的 proxy 對象加入映射表中,得到 object.id 填充到 args 中 。然后,再把 message,opcode,args 等組好 wl_closure ,序列化完成后,通過 socket 發給服務端。

再回到服務端,收到客戶端消息后,進入回調函數 wl_client_connection_data,反序列化消息,得到 wl_closure,找到目標對象對應的接口函數,利用 libffi 執行 server 端的函數: display_get_registry,這個函數會在 server 端創建對應的 wl_resource 也就是 registry_resource,將它 set_implementation,也就是 registry_bind。然后再遍歷可用的 global 對象發信號 global。

    wl_list_for_each(global, &display->global_list, link)
        if (wl_global_is_visible(client, global) && !global->removed)
            wl_resource_post_event(registry_resource,
                           WL_REGISTRY_GLOBAL,
                           global->name,
                           global->interface->name,
                           global->version);

這樣,第二個對象 registry 在服務端和客戶端的 request 和 event 也都設置好了。

進程間的函數調用

當服務端發出 global 信號時,客戶端程序就可以 wl_registry_bind 這些對象,生成本地的可用對象 wl_proxy。[參考 weston 的客戶程序代碼 window.c]
但是信號機制一般是對同一個進程來說的,我們可以監聽某個對象的某個信號,當收到信號時執行對應的回調函數;而這里其實是兩個程序,服務端和客戶端,這種跨進程的信號不是簡單地 connect 就可以的,而是需要通過 socket 來傳遞。
要讓兩個進程通過 socket 進行函數調用,首先需要將調用抽象成數據流的形式,這些信息通過 wl_closure_marshal 寫入 wl_closure 結構,再由 serialize_closure 變成數據流;等到了目標進程,從數據流中通過 wl_connection_demarshal 轉回 wl_closure 結構。
如果參數中不止整型字符串等簡單類型,還存在對象的話,就需要 wl_map 來做這個對象在 server 和 client 端的映射了。
上面所謂的發出 global 信號,其實也就是把函數調用的請求序列化,然后再發給客戶端程序,客戶端收到消息,反序列化,解析出object id, 從 wl_map 中找到本地可用對象,拿到 opcode,再解析本地對象的 implementation 對應 opcode 的函數,也就是執行 registry_handle_global。

    if (strcmp(interface, "wl_compositor") == 0) {
        d->compositor = wl_registry_bind(registry, id,
                         &wl_compositor_interface, 3);
    } else if (strcmp(interface, "wl_output") == 0) {
        display_add_output(d, id);
    } else if (strcmp(interface, "wl_seat") == 0) {
        display_add_input(d, id, version);
    }

同樣的,這個 wl_registry_bind 也是把客戶端的請求消息序列化后,發給服務端;服務端收到后反序列化,執行對應的 bind,也就是 wayland-server.c 中定義的 registry_bind

整個流程就是 client 端通過 wl_proxy 調用 server 端的 wl_resource 的 request,server 端通過 wl_resource 調用 client 端的 wl_proxy 的 event。

再簡單說明一下客戶端收到消息的處理流程,wl_display_dispatch -> wl_display_read_events -> read_events -> queue_event,讀取數據,將解析出來的 event 加入 event_list 中。
在 dispatch_queue 中 dispatch_event,也就是執行 wl_closure_invoke。具體的下次再說吧……

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容