频道栏目
首页 > 资讯 > 虚拟机 > 正文

Art 虚拟机的启动

17-12-21        来源:[db:作者]  
收藏   我要投稿

??在 Android 系统中,应用程序进程是由 zygote 进程 fork 出来的。zygote 进程在启动时会创建一个 Art 虚拟机实例,每当它 fork 一个新的应用程序进程时,也会将这个 Art 虚拟机实例复制到新的应用程序进程里,从而使得每一个应用程序进程都有一个独立的 Art 虚拟机实例。

一、zygote 的启动

由 init.cpp 加载解析 init.rc 文件,init.rc 文件中会有 import /init.${ro.zygote}.rc,${ro.zygote} 是 property 的值,可以看到有四种情况分别是:zygote32、zygote32_64、zygote64 以及 zygote64_32,下面我们分别看一下对应的几个 .rc 文件中的内容是什么:
init.zygote32.rc

service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

init.zygote32_64.rc

service zygote /system/bin/app_process32 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

service zygote_secondary /system/bin/app_process64 -Xzygote /system/bin --zygote --socket-name=zygote_secondary
    class main
    socket zygote_secondary stream 660 root system
    onrestart restart zygote
    writepid /dev/cpuset/foreground/tasks

init.zygote64.rc

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

init.zygote64_32.rc

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary
    class main
    socket zygote_secondary stream 660 root system
    onrestart restart zygote
    writepid /dev/cpuset/foreground/tasks

init language 的含义请参考博客 Android Init Language
可以看到上面四份 .rc 文件的差别在于创建几个 zygote service,以及主要的 zygote service 的创建入口是 app_process64 还是 app_process32

下面我们以 app_process64 为例,看一下 zygote 是如何启动的

 

1.1 app_main.cpp

/frameworks/base/cmds/app_process/Android.mk 中可以看出,不论是 app_process32 还是 app_process64,实际上他们对应的代码都在 /frameworks/base/cmds/app_process/app_main.cpp 中,下面看一下其中的 main() 方法:

int main(int argc, char* const argv[])
{
    ...
    // 1. 创建 runtime
    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
    ...
    // 2. 解析参数
    // Parse runtime arguments.  Stop at first unrecognized option.
    bool zygote = false;
    bool startSystemServer = false;
    bool application = false;
    String8 niceName;
    String8 className;

    ++i;  // Skip unused "parent dir" argument.
    while (i < argc) {
        const char* arg = argv[i++];
        if (strcmp(arg, "--zygote") == 0) {
            zygote = true;
            niceName = ZYGOTE_NICE_NAME; // "zygote64"
        } else if (strcmp(arg, "--start-system-server") == 0) {
            startSystemServer = true;
        } else if (strcmp(arg, "--application") == 0) {
            application = true;
        } else if (strncmp(arg, "--nice-name=", 12) == 0) {
            niceName.setTo(arg + 12);
        } else if (strncmp(arg, "--", 2) != 0) {
            className.setTo(arg);
            break;
        } else {
            --i;
            break;
        }
    }

    if (zygote) { // 此种情况下 zygote 为 true
        // 3. runtime.start
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
        runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
        fprintf(stderr, "Error: no class name or --zygote supplied.\n");
        app_usage();
        LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
        return 10;
    }
}

从上可以看出,main() 方法中主要有三步,分别是:

创建 AppRuntime 解析参数 启动 AppRuntime

1.1.1 AppRuntime

class AppRuntime : public AndroidRuntime
{
public:
    AppRuntime(char* argBlockStart, const size_t argBlockLength)
        : AndroidRuntime(argBlockStart, argBlockLength)
        , mClass(NULL)
    {
    }
}

从中可以看出 AppRuntime 继承于 AndroidRuntime,构造函数主要也是执行的 AndroidRuntime 的构造函数。

1.2 AndroidRuntime.cpp

AndroidRuntime.cpp 的路径位于 /frameworks/base/core/jni/AndroidRuntime.cpp

1.2.1 AndroidRuntime::AndroidRuntime

AndroidRuntime::AndroidRuntime(char* argBlockStart, const size_t argBlockLength) :
        mExitWithoutCleanup(false),
        mArgBlockStart(argBlockStart),
        mArgBlockLength(argBlockLength)
{
    SkGraphics::Init();
    mOptions.setCapacity(20);

    assert(gCurRuntime == NULL);        // one per process
    gCurRuntime = this;
}

可以看到其构造函数主要就是一些初始化

1.2.2 AndroidRuntime::start

AndroidRuntime.cpp

void AndroidRuntime::start(const char* className, const Vector& options, bool zygote)
{
    ...

    /* start the virtual machine */
    JniInvocation jni_invocation;
    jni_invocation.Init(NULL);
    JNIEnv* env;
    // 1. startVm(&mJavaVM, &env, zygote)
    if (startVm(&mJavaVM, &env, zygote) != 0) {
        return;
    }
    onVmCreated(env);

    /*
     * Register android functions.
     */
    // 2. startReg(env)
    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }

    /*
     * We want to call main() with a String array with arguments in it.
     * At present we have two arguments, the class name and an option string.
     * Create an array to hold them.
     */
    jclass stringClass;
    jobjectArray strArray;
    jstring classNameStr;

    stringClass = env->FindClass("java/lang/String");
    assert(stringClass != NULL);
    strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL);
    assert(strArray != NULL);
    classNameStr = env->NewStringUTF(className);
    assert(classNameStr != NULL);
    env->SetObjectArrayElement(strArray, 0, classNameStr);

    for (size_t i = 0; i < options.size(); ++i) {
        jstring optionsStr = env->NewStringUTF(options.itemAt(i).string());
        assert(optionsStr != NULL);
        env->SetObjectArrayElement(strArray, i + 1, optionsStr);
    }

    /*
     * Start VM.  This thread becomes the main thread of the VM, and will
     * not return until the VM exits.
     */
    char* slashClassName = toSlashClassName(className);
    jclass startClass = env->FindClass(slashClassName);
    if (startClass == NULL) {
        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
        /* keep going */
    } else {
        jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
            "([Ljava/lang/String;)V");
        if (startMeth == NULL) {
            ALOGE("JavaVM unable to find main() in '%s'\n", className);
            /* keep going */
        } else {
            // 3. 调用 "className" 的 "static void main(String[] args)" 方法
            env->CallStaticVoidMethod(startClass, startMeth, strArray);

#if 0
            if (env->ExceptionCheck())
                threadExitUncaughtException(env);
#endif
        }
    }
    free(slashClassName);

    ALOGD("Shutting down VM\n");
    if (mJavaVM->DetachCurrentThread() != JNI_OK)
        ALOGW("Warning: unable to detach main thread\n");
    if (mJavaVM->DestroyJavaVM() != 0)
        ALOGW("Warning: VM did not shut down cleanly\n");
}

这个方法的作用是启动 Android runtime,主要涉及到两方面的操作:

启动虚拟机,包括:
创建一个 JniInvocation 实例,并且调用它的成员函数 init 来初始化 JNI 环境; startVm(&mJavaVM, &env, zygote) startReg(env) 调用传过来的参数 “className” 的 “static void main(String[] args)” 方法

下面我们分别看一下在 startVm() 和 startReg() 中,系统做了什么

1.3 startVm

1.3.1 AndroidRuntime::startVm

AndroidRuntime.cpp

int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
    JavaVMInitArgs initArgs;
    enum {
      kEMDefault,
      kEMIntPortable,
      kEMIntFast,
      kEMJitCompiler,
    } executionMode = kEMDefault;
    ...
    bool checkJni = false;

    ALOGD("CheckJNI is %s\n", checkJni ? "ON" : "OFF");
    if (checkJni) {
        /* extended JNI checking */
        addOption("-Xcheck:jni");
    }
    ...
    property_get("dalvik.vm.execution-mode", propBuf, "");
    if (strcmp(propBuf, "int:portable") == 0) {
        executionMode = kEMIntPortable;
    } else if (strcmp(propBuf, "int:fast") == 0) {
        executionMode = kEMIntFast;
    } else if (strcmp(propBuf, "int:jit") == 0) {
        executionMode = kEMJitCompiler;
    }
    ...
    if (executionMode == kEMIntPortable) {
        addOption("-Xint:portable");
    } else if (executionMode == kEMIntFast) {
        addOption("-Xint:fast");
    } else if (executionMode == kEMJitCompiler) {
        addOption("-Xint:jit");
    }
    ...
    initArgs.version = JNI_VERSION_1_4;
    initArgs.options = mOptions.editArray();
    initArgs.nOptions = mOptions.size();
    initArgs.ignoreUnrecognized = JNI_FALSE;

    /*
     * Initialize the VM.
     *
     * The JavaVM* is essentially per-process, and the JNIEnv* is per-thread.
     * If this call succeeds, the VM is ready, and we can start issuing
     * JNI calls.
     */
    if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
        ALOGE("JNI_CreateJavaVM failed\n");
        return -1;
    }

    return 0;
}

从中可以看出,这里主要做了两件事:

解析所需的 properties 和各种 options,并将所有 options 添加到 mOptions 中 调用 JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) 创建 JavaVM,其中 initArgs 是一个 JavaVMInitArgs 对象,其包含 mOptions 等信息

1.3.2 JNI_CreateJavaVM

JniInvocation.cpp

extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
  return JniInvocation::GetJniInvocation().JNI_CreateJavaVM(p_vm, p_env, vm_args);
}

jint JniInvocation::JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
  return JNI_CreateJavaVM_(p_vm, p_env, vm_args);
}

可以看到其实际上是调用了 JNI_CreateJavaVM_ 方法
JniInvocation.h

  jint (*JNI_CreateJavaVM_)(JavaVM**, JNIEnv**, void*);

根据声明可以看出 JNI_CreateJavaVM_ 实际上是一个指向函数的指针,下面我们看一下 JNI_CreateJavaVM_ 指向哪里:
JniInvocation.cpp

JniInvocation::JniInvocation() :
    handle_(NULL),
    JNI_GetDefaultJavaVMInitArgs_(NULL),
    JNI_CreateJavaVM_(NULL),
    JNI_GetCreatedJavaVMs_(NULL) {

  LOG_ALWAYS_FATAL_IF(jni_invocation_ != NULL, "JniInvocation instance already initialized");
  jni_invocation_ = this;
}

bool JniInvocation::Init(const char* library) {
  ...
  // 可以看到非 debug 版本,library 为 "libart.so"
  library = GetLibrary(library, buffer);
  ...
  const int kDlopenFlags = RTLD_NOW | RTLD_NODELETE;
  handle_ = dlopen(library, kDlopenFlags);
  if (handle_ == NULL) {
    if (strcmp(library, kLibraryFallback) == 0) {
      // Nothing else to try.
      ALOGE("Failed to dlopen %s: %s", library, dlerror());
      return false;
    }
    ...
    library = kLibraryFallback;
    handle_ = dlopen(library, kDlopenFlags);
    if (handle_ == NULL) {
      ALOGE("Failed to dlopen %s: %s", library, dlerror());
      return false;
    }
  }
  if (!FindSymbol(reinterpret_cast(&JNI_GetDefaultJavaVMInitArgs_),
                  "JNI_GetDefaultJavaVMInitArgs")) {
    return false;
  }
  if (!FindSymbol(reinterpret_cast(&JNI_CreateJavaVM_),
                  "JNI_CreateJavaVM")) {
    return false;
  }
  if (!FindSymbol(reinterpret_cast(&JNI_GetCreatedJavaVMs_),
                  "JNI_GetCreatedJavaVMs")) {
    return false;
  }
  return true;
}

dlopen:

函数定义: void * dlopen( const char * pathname, int mode ); 函数描述: 函数以指定模式打开指定的动态连接库文件,并返回一个句柄给调用进程。使用 dlclose() 来卸载打开的库。

由此可知 handle_ = dlopen(library, kDlopenFlags); 这里的作用是以 kDlopenFlags 的模式打开 “libart.so” 库,并将返回的句柄保存到 handle_ 中

我们再看一下 FindSymbol 的实现:

bool JniInvocation::FindSymbol(void** pointer, const char* symbol) {
  *pointer = dlsym(handle_, symbol);
  if (*pointer == NULL) {
    ALOGE("Failed to find symbol %s: %s\n", symbol, dlerror());
    dlclose(handle_);
    handle_ = NULL;
    return false;
  }
  return true;
}

dlsym:

函数定义:void * dlsym(void* handle, const char* symbol); 函数描述:dlsym 根据动态链接库操作句柄 handle 与符号 symbol,返回符号对应的地址,使用这个函数不但可以获取函数地址,也可以获取变量地址

由此可知,这里 FindSymbol 的作用就是将函数指针指向动态库中对应名称的函数

“JNI_CreateJavaVM” 在 “libart.so” 库中对应的实现为:
java_vm_ext.cc

extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) {
  ScopedTrace trace(__FUNCTION__);
  const JavaVMInitArgs* args = static_cast(vm_args);
  ...
  // 1. 调整 options
  RuntimeOptions options;
  for (int i = 0; i < args->nOptions; ++i) {
    JavaVMOption* option = &args->options[i];
    options.push_back(std::make_pair(std::string(option->optionString), option->extraInfo));
  }
  bool ignore_unrecognized = args->ignoreUnrecognized;
  // 2. 创建 Runtime
  if (!Runtime::Create(options, ignore_unrecognized)) {
    return JNI_ERR;
  }

  // 3. 创建 native loader,这一步确保了在我们使用 JNI 之前,所有的事情都被准备好
  android::InitializeNativeLoader();

  Runtime* runtime = Runtime::Current();
  // 4. 启动 runtime
  bool started = runtime->Start();
  if (!started) {
    delete Thread::Current()->GetJniEnv();
    delete runtime->GetJavaVM();
    LOG(WARNING) << "CreateJavaVM failed";
    return JNI_ERR;
  }
  // 5. 把创建好的 JniEnv 和 JavaVM 分别赋给 *p_env 和 *p_vm
  *p_env = Thread::Current()->GetJniEnv();
  *p_vm = runtime->GetJavaVM();
  return JNI_OK;
}

此函数主要的作用见注释

1.4 startReg

AndroidRuntime.cpp

/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
    ATRACE_NAME("RegisterAndroidNatives");
    /*
     * This hook causes all future threads created in this process to be
     * attached to the JavaVM.  (This needs to go away in favor of JNI
     * Attach calls.)
     */
    androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);

    ALOGV("--- registering native functions ---\n");

    /*
     * Every "register" function calls one or more things that return
     * a local reference (e.g. FindClass).  Because we haven't really
     * started the VM yet, they're all getting stored in the base frame
     * and never released.  Use Push/Pop to manage the storage.
     */
    env->PushLocalFrame(200);

    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    env->PopLocalFrame(NULL);

    //createJavaThread("fubar", quickTest, (void*) "hello");

    return 0;
}

这个方法的作用是在 VM 中注册安卓 native 方法,下面我们看一下 register_jni_procs 方法的实现:

1.4.1 register_jni_procs

AndroidRuntime.cpp

static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
    for (size_t i = 0; i < count; i++) {
        if (array[i].mProc(env) < 0) {
#ifndef NDEBUG
            ALOGD("----------!!! %s failed to load\n", array[i].mName);
#endif
            return -1;
        }
    }
    return 0;
}

其中传过来的参数 gRegJNI 的定义如下所示:

static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_android_util_SeempLog),
    REG_JNI(register_com_android_internal_os_RuntimeInit),
    REG_JNI(register_android_os_SystemClock),
    REG_JNI(register_android_util_EventLog),
    ...
};    
#ifdef NDEBUG
    #define REG_JNI(name)      { name }
    struct RegJNIRec {
        int (*mProc)(JNIEnv*);
    };
#else
    #define REG_JNI(name)      { name, #name }
    struct RegJNIRec {
        int (*mProc)(JNIEnv*);
        const char* mName;
    };
#endif

由此可知,register_jni_procs 方法实际上就是依次调用 gRegJNI[] 中对应的 register 方法;例如针对 register_com_android_internal_os_RuntimeInit,实际上就是调用 register_com_android_internal_os_RuntimeInit(env);

其定义如下所示:

1.4.2 register_com_android_internal_os_RuntimeInit

AndroidRuntime.cpp

int register_com_android_internal_os_RuntimeInit(JNIEnv* env)
{
    return jniRegisterNativeMethods(env, "com/android/internal/os/RuntimeInit",
        gMethods, NELEM(gMethods));
}

/*
 * JNI registration.
 */
static const JNINativeMethod gMethods[] = {
    { "nativeFinishInit", "()V",
        (void*) com_android_internal_os_RuntimeInit_nativeFinishInit },
    { "nativeZygoteInit", "()V",
        (void*) com_android_internal_os_RuntimeInit_nativeZygoteInit },
    { "nativeSetExitWithoutCleanup", "(Z)V",
        (void*) com_android_internal_os_RuntimeInit_nativeSetExitWithoutCleanup },
};

我们看一下 JNINativeMethod 的声明:
jni.h

typedef struct {
    const char* name; // Java 层 native 函数名
    const char* signature; // Java 函数签名,记录参数类型和个数,以及返回值类型
    void*       fnPtr; // Native 层对应的函数指针
} JNINativeMethod;

1.4.3 jniRegisterNativeMethods

JNIHelp.cpp

extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    JNIEnv* e = reinterpret_cast(env);

    scoped_local_ref c(env, findClass(env, className));
    ...
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        char* tmp;
        const char* msg;
        if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
            // Allocation failed, print default warning.
            msg = "RegisterNatives failed; aborting...";
        } else {
            msg = tmp;
        }
        e->FatalError(msg);
    }

    return 0;
}

由此可知,这里的主要作用是通过 RegisterNatives 方法来注册 native 方法

1.4.4 RegisterNatives

jni_internal.cc

  static jint RegisterNatives(JNIEnv* env, jclass java_class, const JNINativeMethod* methods,
                              jint method_count) {
    return RegisterNativeMethods(env, java_class, methods, method_count, true);
  }

  static jint RegisterNativeMethods(JNIEnv* env, jclass java_class, const JNINativeMethod* methods,
                              jint method_count, bool return_errors) {
    ...
    ScopedObjectAccess soa(env);
    mirror::Class* c = soa.Decode(java_class);
    ...
    for (jint i = 0; i < method_count; ++i) {
      // 对 methods 中的每个成员分别进行注册
      const char* name = methods[i].name;
      const char* sig = methods[i].signature;
      const void* fnPtr = methods[i].fnPtr;
      // 检测 name、signature、fnPtr 是否为空
      ...
      bool is_fast = false;
      // 为 fast JNI call 时,signature 有前缀 '!'
      if (*sig == '!') {
        is_fast = true;
        ++sig;
      }

      // Note: the right order is to try to find the method locally
      // first, either as a direct or a virtual method. Then move to
      // the parent.
      ArtMethod* m = nullptr;
      bool warn_on_going_to_parent = down_cast(env)->vm->IsCheckJniEnabled();
      for (mirror::Class* current_class = c;
           current_class != nullptr;
           current_class = current_class->GetSuperClass()) {
        // 在对应的 Java 类中寻找 native 函数名对应的 ArtMethod
        // Search first only comparing methods which are native.
        m = FindMethod(current_class, name, sig);
        if (m != nullptr) {
          break;
        }

        // Search again comparing to all methods, to find non-native methods that match.
        m = FindMethod(current_class, name, sig);
        if (m != nullptr) {
          break;
        }
        ...
      }

      if (m == nullptr) {
        LOG(return_errors ? ERROR : INTERNAL_FATAL) << "Failed to register native method "
            << PrettyDescriptor(c) << "." << name << sig << " in "
            << c->GetDexCache()->GetLocation()->ToModifiedUtf8();
        // Safe to pass in LOG(FATAL) since the log object aborts in destructor and only goes
        // out of scope after the DumpClass is done executing.
        c->DumpClass(LOG(return_errors ? ERROR : FATAL), mirror::Class::kDumpClassFullDetail);
        ThrowNoSuchMethodError(soa, c, name, sig, "static or non-static");
        return JNI_ERR;
      } else if (!m->IsNative()) {
        LOG(return_errors ? ERROR : FATAL) << "Failed to register non-native method "
            << PrettyDescriptor(c) << "." << name << sig
            << " as native";
        ThrowNoSuchMethodError(soa, c, name, sig, "native");
        return JNI_ERR;
      }

      VLOG(jni) << "[Registering JNI native method " << PrettyMethod(m) << "]";
      // 调用 RegisterNative 方法来继续进行注册
      m->RegisterNative(fnPtr, is_fast);
    }
    return JNI_OK;
  }

从中可以看出这个方法会分别解析 methods 中的每一个 JNINativeMethod 结构体,从而将其中的 native 方法与对应的 Java 层 native 函数名联系在一起,剩下的部分不再进行深入探究

1.5 ZygoteInit.main()

在 1.2.2 节的第三步中,会通过

env->CallStaticVoidMethod(startClass, startMeth, strArray);

调用 “className” 的 “static void main(String[] args)” 方法,即调用 ZygoteInit 的 main() 方法,开启 zygote 初始化的后续过程。

相关TAG标签
上一篇:Java字符串整数工具类讲解
下一篇:git 应用 cherry-pick详情
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站