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