1. 程式人生 > >Android Fk: PKMS(3)之installd及LocalSocket實現Java層與Native層通訊

Android Fk: PKMS(3)之installd及LocalSocket實現Java層與Native層通訊

LOCAL_CLANG := true#Android Fk: PKMS(3)之installd及LocalSocket實現Java層與Native層通訊

一、installd的概述

  從上一篇介紹應用安裝與解除安裝的學習文件中知道PKMS在實現部分包管理功能時需要藉助installd去完成,關於呼叫的詳細流程可以參考這篇部落格,Android7.0 PackageManagerService (5) installd,作者詳細介紹了installd的初始化及呼叫方法的流程,檢視android 7.1.1的原始碼,這部分程式碼和博主所述大致一致,本人就不贅述了。

1.installd的啟動

  installd是個native的服務,在system/bin下,開機時由init啟動:
這裡寫圖片描述

  看installd的rc檔案:

service installd /system/bin/installd
    class main
    socket installd stream 600 system system

  可以看到在起installd的時候建立了一個名為installd的socket檔案,檢視如下:
這裡寫圖片描述
  看到dev/socket下還有其他服務建立的socket檔案;

  由上面提到的部落格分析得知 installd啟動後,獲取作為服務端的socket “installd”; 然後,監聽”installd”,等待Java層installer服務的連線及命令的到來:

2.installd的呼叫方試

  作為客戶端的PKMS使用Intaller中封裝好的用於socket通訊的InstallerConnection對應向對應的socket”installd”發生操作指令;
  PKMS調到installd的大致流程總結如圖(詳細流程參考上面提到的部落格):
  這裡寫圖片描述
  PKMS呼叫installd的方式是通過localsocket的方式實現的,socket實現了從Java層到Native層的通訊,下面將通過一個demo學習來使用下這種socket通訊方式;

二、Socket方式實現Java層與Native層通訊

1.模仿installd寫一個開機啟動的native服務

1.1 native服務原始碼

  在framework/base/cmd下新建一個demo資料夾命名為socket_test,或者在installd的模組目錄framework/native/cmd下建專案目錄也可以,然後新建c++檔案,socket_test.cpp,如下:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/un.h>
#include <cutils/sockets.h>
#include <utils/Log.h>
#include <android/log.h>

#define SOCKET_NAME "socket_test"
#define  LOG_TAG    "SOCKET_TEST_SERVER"
#define  LOGD(...)  __android_log_write(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

int main(){
    char log[200];
    LOGD("main");
    int connect_number = 6;
    int fdListen = -1, new_fd = -1;
    int ret;
    struct sockaddr_un peeraddr;
    socklen_t socklen = sizeof (peeraddr);
    int numbytes ;
    char buff[256];
    //獲取SOCKET_NAME的socket檔案描述符
    fdListen = android_get_control_socket(SOCKET_NAME);
    if (fdListen < 0) {
        sprintf(log,"Failed to get socket '" SOCKET_NAME "' errno:%d , main listen will exit!", errno);
        LOGD(log);
        exit(-1);
    }
    //監聽客戶端連線,最多連線connect_number個
    ret = listen(fdListen, connect_number);
    sprintf(log,"Listen result %d",ret);
    LOGD(log);

    if (ret < 0) {
        perror("listen");
        exit(-1);
    }
    //獲取客戶端的連線
    new_fd = accept(fdListen, (struct sockaddr *) &peeraddr, &socklen);
    sprintf(log,"Accept_fd: %d",new_fd);
    LOGD(log);
    if (new_fd < 0 ) {
        sprintf(log,"fd<0 error %d",errno);
        LOGD(log);
        exit(-1);
    }

    while(1) {
        LOGD("Waiting for Client ...");
        if((numbytes = recv(new_fd,buff,sizeof(buff),0))==-1) {
            sprintf(log,"%d",errno);
            LOGD(log);
            continue;
        } else {
            sprintf(log,"Server Received: %s",buff);
            LOGD(log);
        }
        //將收到的buff資訊再send回給client端
        if(send(new_fd,buff,strlen(buff),0)==-1) {
            close(new_fd);
            LOGD("send error!");
            exit(0);
        } else {
            LOGD("Server sendback succuess!");
        }
    }
    LOGD("main close ");
    close(new_fd);
    close(fdListen);
    return 0;
}

  主要流程和installd類似,大概的操作如下:
  啟動後獲取對應的socket檔案描述符作為socket的server端,然後監聽是否有client連線,client連線後接受client傳送的訊息,然後將訊息再通過socket方式send回給client端;

1.2 Android.mk檔案

  然後同目錄下新建Android.mk檔案,如下:

#frameworks/base/cmds/socket_test/Android.mk
LOCAL_PATH:= $(call my-dir)
common_src_files := socket_test.cpp
include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(common_src_files)
LOCAL_CFLAGS += -DGL_GLEXT_PROTOTYPES -DEGL_EGLEXT_PROTOTYPES
LOCAL_SHARED_LIBRARIES := \
    libcutils \
    liblog \
    libandroidfw \
    libutils \
    libselinux 

LOCAL_MODULE := socket_test
LOCAL_INIT_RC := socket_test.rc
include $(BUILD_EXECUTABLE)

  其中值得注意的是LOCAL_INIT_RC這個標識,由於需要讓這個demo服務通過init啟動,因此需要給socket_test寫自己的rc檔案,有LOCAL_INIT_RC標識,在編該模組的時候會將該rc檔案拷貝到system/etc/init目錄下,init會去解析這個目錄下的所以rc檔案然後做對應的操作;

1.3 socket_test.rc檔案

  接著定義socket_test的rc檔案:

service socket_test /system/bin/socket_test
    class main
    socket socket_test stream 660 system system
#保證開機結束後啟動
on property:sys.boot_completed=1
    start socket_test

  在這裡方便驗證,直接將這個socket_init.rc檔案push到system/etc/init/下面,但是需要在重啟前將rc檔案許可權改為和其他rc檔案一致,否則可能導致服務起不來,甚至無法開機,可以看到許可權改為644:
這裡寫圖片描述

adb root
adb remount
adb push XX/socket_test.rc system/etc/init/
adb shell
chmod 644 system/etc/init/socket_test.rc

  到這一步我們可以嘗試make socket_test -j8是否生成了對應的服務,這一步會在”out/target/product/pollux/system/bin/”生成socket_test服務,另外會將socket_test.rc更新到”out/target/product/pollux/system/etc/init/”目錄下,全編的時候會將這個目錄打包到手機對應的system/etc/init/目錄下,然後開機去解析;
  將該服務push到system/bin/下,然後更改許可權,
這裡寫圖片描述

adb push out/target/product/pollux/system/bin/socket_test system/bin
adb chmod 755 socket_test

1.4 小問題解決

1.4.1 SELinux domain未定義導致socket_test未啟動
  滿懷激動的重啟後,發現socket_test並未啟動,而且dev/socket下也沒有生成rc中定義好的socket檔案,
檢視開機log發現如下:

03-08 10:00:19.212502     0     0 E init    : Service socket_test does not have a SELinux domain defined.

  說明沒有定義SELinux domain,導致服務無法自啟動。需按如下方式修改或新增sepolicy檔案:
   a. 在system/sepolicy/file_contexts檔案末尾新增

#############################
# socket_test
# System files
/system/bin/socket_test u:object_r:socket_test_exec:s0

   b.在system/sepolicy/資料夾下新建socket_test.te檔案,內容如下:

type socket_test, domain;
type socket_test_exec, exec_type, file_type;
init_daemon_domain(socket_test)

   c.編譯bootimage,燒錄bootimage,執行如下命令後再重啟檢視socket_test程序是否起來:

adb root
adb remount
adb shell restorecon system/bin/xxx
adb reboot

1.4.2 selinux問題導致socket檔案未建立,然後建立了socket檔案server又無法獲取等問題
  但是看到dev/socket/下socket_test可能還是沒有被建立,這裡其實是selinux的問題了,因此需要根據log裡顯示被拒許可權相應的新增這些許可權;
  類似於如下的log:

03-08 10:31:04.219000  3738  3738 I auditd  : type=1400 audit(0.0:645): avc: denied { create } for comm="init" name="socket_test" scontext=u:r:init:s0 tcontext=u:object_r:socket_device:s0 tclass=sock_file permissive=1

03-08 10:31:08.719000  4181  4181 W socket_test: type=1400 audit(0.0:650): avc: denied { read write } for path="/dev/oeminfo" dev="tmpfs" ino=17725 scontext=u:r:socket_test:s0 tcontext=u:object_r:oeminfo_device:s0 tclass=chr_file permissive=1

03-08 11:24:27.919  3711  3711 I auditd  : type=1400 audit(0.0:666): avc: denied { setattr } for comm="init" name="socket_test" dev="tmpfs" ino=35596 scontext=u:r:init:s0 tcontext=u:object_r:socket_device:s0 tclass=sock_file permissive=0

  後面還會有其他奇怪的問題,比如socket檔案建立好了,單socket_test服務無法獲取dev/socket/下的socket_test檔案作為服務端,log顯示:

03-08 11:11:31.384     0     0 E init    : Failed to lchown socket '/dev/socket/socket_test': Permission denied
看到該log前面一點出現
03-08 11:24:27.919  3711  3711 W init    : type=1400 audit(0.0:666): avc: denied { setattr } for name="socket_test" dev="tmpfs" ino=35596 scontext=u:r:init:s0 tcontext=u:object_r:socket_device:s0 tclass=sock_file permissive=1

  所以這同樣是SElinux問題,新增selinux許可權的方法如下,在system/sepolicy/資料夾下新增的socket_test.te末尾新增許可權:

#allow scontext tcontext:tclass { perm1 perm2 } 大致的新增方式
allow init socket_device:sock_file{ create unlink link setattr};
allow socket_test oeminfo_device:chr_file { read write };
allow socket_test rootfs:lnk_file { getattr setattr };

  selinux也是修改了system/sepolicy中的檔案,同樣如上編bootimage,燒錄,restorecon操作,重啟,
現在終於看到dev/socket/下有socket_test檔案了,system/bin/下的socket_test服務同樣也起來了,而且也已成功獲取socket_test檔案作為服務端,此時正在監聽client端連線。
這裡寫圖片描述

這裡寫圖片描述

  至此模仿installd,完成了native層的服務端,下面來完成java層的client短。

2. 寫個Apk作為Client端向Native 服務 socket_test進行通訊

2.1 apk核心程式碼

  為了操作方便直接寫成apk作為client端,一個訊息輸入框,一個傳送按鈕,一個顯示server發回的訊息;
  生成系統簽名的該apk,push到system/app下,給apk以系統的shareUid:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.demo.mysocketclient"
    android:sharedUserId="android.uid.system">

  主要程式碼:

public class MainActivity extends AppCompatActivity {
    Button btn_send;
    EditText etxt;
    TextView txtRev;
    private final String SOCKET_NAME = "socket_test";
    LocalSocket client;
    LocalSocketAddress address;
    private InputStream mIn;
    private OutputStream mOut;
    BufferedReader in;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        client = new LocalSocket();
        address = new LocalSocketAddress(SOCKET_NAME, LocalSocketAddress.Namespace.RESERVED);
        btn_send = (Button) findViewById(R.id.btn_send);
        etxt = (EditText) findViewById(R.id.etxt_msg);
        txtRev = (TextView) findViewById(R.id.txt_receved);
        try {
            client.connect(address);
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            mOut = client.getOutputStream();
            mIn = client.getInputStream();
            in = new BufferedReader(new InputStreamReader(mIn));
        } catch (IOException e) {
            e.printStackTrace();
        }

        btn_send.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //這裡需要在末尾加個換行符,否則在server傳送返回回來的時候下面的in.readLine()會阻塞住
                String msg = etxt.getText().toString() + "\n";
                String rev = SendMsg(msg);
                if (rev != null) {
                    txtRev.setText(rev.toString());
                } else {
                    Log.d("DCYY", "rev is null!");
                }
            }
        });
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (client != null && client.isConnected()) {
            try {
                client.close();
                mOut.close();
                mIn.close();
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private String SendMsg(String s) {
        final byte[] message = s.getBytes();
        final int len = message.length;
        if ((len < 1))
            return "empty msg!";
        try {
            //向socket服務端傳送訊息
            mOut.write(message);
            SystemClock.sleep(1000);
            return in.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "send failed!";
    }
}

2.2 小問題解決

  這裡selinux許可權還有衝突,比如app需要獲取連線到dev/socket/下面的socket_test檔案用於socket通訊,但是卻被Selinux許可權拒絕了,每次要進行socket連線的時候總是報錯,顯示的log如下:

08-06 19:19:29.654 5964-5964/com.demo.mysocketclient W/.mysocketclient: type=1400 audit(0.0:1035): avc: denied { write } for name="socket_test" dev="tmpfs" ino=519 scontext=u:r:system_app:s0 tcontext=u:object_r:socket_device:s0 tclass=sock_file permissive=0
08-06 19:19:29.664 5964-5964/com.demo.mysocketclient W/System.err: java.io.IOException: Permission denied
08-06 19:19:29.664 5964-5964/com.demo.mysocketclient W/System.err:     at android.net.LocalSocketImpl.connectLocal(Native Method)
08-06 19:19:29.664 5964-5964/com.demo.mysocketclient W/System.err:     at android.net.LocalSocketImpl.connect(LocalSocketImpl.java:292)
08-06 19:19:29.665 5964-5964/com.demo.mysocketclient W/System.err:     at android.net.LocalSocket.connect(LocalSocket.java:131)
08-06 19:19:29.665 5964-5964/com.demo.mysocketclient W/System.err:     at com.demo.mysocketclient.MainActivity.onCreate(MainActivity.java:40)

  加上WRITE_EXTERNAL_STORAGE這個許可權都不管用,這是selinux問題,對照上面新增上selinux許可權:

allow system_app socket_device:sock_file{ read write };
allow system_app socket_test:unix_stream_socket{ connectto };

  可是發現編bootimage又編不過了,原來app.te規定了appdomain是不允許有這個許可權的,衝突導致編不過bootimage了。

system/sepolicy/app.te
# Sockets under /dev/socket that are not specifically typed.
neverallow appdomain socket_device:sock_file write;

  所以還是先偷個懶把selinux關了,日後再找方法解決這個問題吧:

adb root
adb remount
adb shell setenforce 0

3. 從Java應用層通過socket方式與Native層service進行通訊

  ok,下面所有selinux問題都不用管了,大膽嘗試:
  在輸入框中寫上要傳送的訊息,然後點擊發送,從log中看的出,此時socket_test是收到了client端發來的訊息了:
這裡寫圖片描述

  從Client端apk介面顯示來看,也收到了由native服務socket_test發回的資訊:
這裡寫圖片描述

  至此,成功的完成從java應用層傳送訊息給native層的服務,並打印出訊息,同時也實現了從native服務層傳送訊息到達java應用層;

三、總結

  1.看到PKMS是通過localsocket的方式與installd進行通訊的,install在收到訊息後根據訊息的指令及資料進行接下來的功能實現,比較靈活。
  2.localsocket是對linux中的socket的封裝,日後再看它其他重要的使用方式已經socket通訊方式的重點;
  3.模仿installd寫了一個本地服務,並通過localsocket方式實現了java應用層與native層的通訊,其實細細想想,這樣的方法可以用來做很多事情,實現自己需要的功能,socket方式也是一種很好的方式。
  4.還有沒有搞定的地方,如果是需要應用在開發中,如何避免selinux禁止appdomain的許可權問題,看到網上說可以把socket檔案生成在data/app/com.xxx.app/下,回頭要好好試試。

參考部落格:
Android7.0 PackageManagerService (5) installd
http://blog.csdn.net/Gaugamela/article/details/52769139
Service xxx does not have a SELinux domain defined
http://blog.csdn.net/l460133921/article/details/72891678
android 6.0 Java層和native守護程序socket通訊
http://blog.csdn.net/u012439416/article/details/72974388
解決avc-denied之設定SELinux策略
http://blog.csdn.net/eliot_shao/article/details/51859083