Android4.4的zygote进程(上)
1背景
前些天为了在科室做培训,我基于Android 4.4重新整理了一份关于zygote的文档。从技术的角度看,这几年zygote并没有出现什么大的变化,所以如果有人以前研究过zygote,应该不会对本文写的内容感到陌生。
2zygote进程的描述
在Android中,zygote是整个系统创建新进程的核心装置。从字面上看,zygote是受精卵的意思,它的主要工作就是进行细胞分裂。
zygote进程在内部会先启动Dalvik虚拟机,继而加载一些必要的系统资源和系统类,最后进入一种监听状态。在后续的运作中,当其他系统模块(比如AMS)希望创建新进程时,只需向zygote进程发出请求,zygote进程监听到该请求后,会相应地“分裂”出新的进程,于是这个新进程在初生之时,就先天具有了自己的Dalvik虚拟机以及系统资源。
系统启动伊始,zygote进程就会被init进程启动起来,init进程的详情可参考我写的《Android4.4的init进程》一文,此处不再赘述。我们直接来看init.rc脚本里的相关描述吧。在这个脚本中是这样描述zygote的:
可以看到,zygote对应的可执行文件就是/system/bin/app_process,也就是说系统启动时会执行到这个可执行文件的main()函数里。
3zygote进程的实现细节
zygote服务的main()函数位于frameworks\base\cmds\app_process\App_main.cpp文件中,其代码截选如下:
- int main(int argc, char* const argv[])
- {
- . . . . . .
- AppRuntime runtime;
- const char* argv0 = argv[0]; // /system/bin/app_process
- argc--;
- argv++;
- . . . . . .
- int i = runtime.addVmArguments(argc, argv); // 会跳过-Xzygote,i的位置对应/system/bin
- . . . . . .
- while (i < argc) {
- const char* arg = argv[i++]; // 应该是/system/bin目录
- if (!parentDir) {
- parentDir = arg;
- } else if (strcmp(arg, "--zygote") == 0) {
- zygote = true;
- niceName = "zygote";
- } else if (strcmp(arg, "--start-system-server") == 0) {
- startSystemServer = true;
- }
- . . . . . .
- }
- if (niceName && *niceName) {
- setArgv0(argv0, niceName);
- set_process_name(niceName); // 一般改名为“zygote”
- }
- runtime.mParentDir = parentDir;
- if (zygote) {
- runtime.start("com.android.internal.os.ZygoteInit",
- startSystemServer ? "start-system-server" : "");
- } else if (className) {
- . . . . . .
- } else {
- . . . . . .
- }
- }
3.1AppRuntime的start()
main()函数里先构造了一个AppRuntime对象,即AppRuntime runtime;而后把进程名改成“zygote”,并利用runtime对象,把工作转交给java层的ZygoteInit类处理。
这个AppRuntime类继承于AndroidRuntime类,却没有重载其start(...)函数,所以main()函数中调用的runtime.start(...)其实走的是AndroidRuntime的start(...),而且传入了类名参数,即字符串——“com.android.internal.os.ZygoteInit”。start()函数的主要代码截选如下:
【frameworks/base/core/jni/AndroidRuntime.cpp】
- void AndroidRuntime::start(const char* className, const char* options)
- {
- . . . . . .
- const char* rootDir = getenv("ANDROID_ROOT");
- . . . . . .
- JniInvocation jni_invocation;
- jni_invocation.Init(NULL); // 初始化JNI接口
- JNIEnv* env;
- if (startVm(&mJavaVM, &env) != 0) { // 启动虚拟机
- return;
- }
- onVmCreated(env);
- if (startReg(env) < 0) { // 注册系统需要的jni函数
- ALOGE("Unable to register all android natives\n");
- return;
- }
- . . . . . .
- jclass startClass = env->FindClass(slashClassName);
- . . . . . .
- jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
- "([Ljava/lang/String;)V");
- . . . . . .
- env->CallStaticVoidMethod(startClass, startMeth, strArray);
- . . . . . .
- }
抛开Java层和C++层的概念,上面的流程说白了就是,Zygote进程的main()函数在启动Dalvik虚拟机后,会调用另一个ZygoteInit类的main()静态函数。调用示意图如下:
3.1.1加载合适的虚拟机动态库
一开始需要初始化JNI接口。
- JniInvocation jni_invocation;
- jni_invocation.Init(NULL);
jni_invocation的init()的代码如下:
【libnativehelper/JniInvocation.cpp】
- bool JniInvocation::Init(const char* library)
- {
- #ifdef HAVE_ANDROID_OS
- char default_library[PROPERTY_VALUE_MAX];
- property_get(kLibrarySystemProperty, default_library, kLibraryFallback);
- #else
- const char* default_library = kLibraryFallback;
- #endif
- if (library == NULL) {
- library = default_library;
- }
- handle_ = dlopen(library, RTLD_NOW);
- . . . . . .
- . . . . . .
- if (!FindSymbol(reinterpret_cast<void**>(&JNI_GetDefaultJavaVMInitArgs_),
- "JNI_GetDefaultJavaVMInitArgs")) {
- return false;
- }
- if (!FindSymbol(reinterpret_cast<void**>(&JNI_CreateJavaVM_),
- "JNI_CreateJavaVM")) {
- return false;
- }
- if (!FindSymbol(reinterpret_cast<void**>(&JNI_GetCreatedJavaVMs_),
- "JNI_GetCreatedJavaVMs")) {
- return false;
- }
- return true;
- }
- property_get(kLibrarySystemProperty, default_library, kLibraryFallback);
具体加载动态库的函数是dlopen(),代码如下:
【bionic/linker/Dlfcn.cpp】
- void* dlopen(const char* filename, int flags) {
- ScopedPthreadMutexLocker locker(&gDlMutex);
- soinfo* result = do_dlopen(filename, flags);
- if (result == NULL) {
- __bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
- return NULL;
- }
- return result;
- }
本文不需细究dlopen()的实现,大家只需知道,它是个强大的库函数,可以打开某个动态库,并将之装入内存。调用dlopen()时传入的第二个参数是RTLD_NOW,它表示加载器会立即计算库的依赖性,从而在dlopen()返回之前,解析出每个未定义变量的地址。
3.1.2启动Dalvik虚拟机,startVm()
初始化JNI环境后,就可以启动Dalvik虚拟机了。下面是startVm()的代码截选:
【frameworks/base/core/jni/AndroidRuntime.cpp】
- int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv)
- {
- . . . . . .
- JavaVMInitArgs initArgs;
- JavaVMOption opt;
- . . . . . .
- . . . . . .
- opt.extraInfo = (void*) runtime_exit;
- opt.optionString = "exit";
- mOptions.add(opt);
- . . . . . .
- // Increase the main thread's interpreter stack size for bug 6315322.
- opt.optionString = "-XX:mainThreadStackSize=24K";
- mOptions.add(opt);
- . . . . . .
- . . . . . .
- initArgs.version = JNI_VERSION_1_4;
- initArgs.options = mOptions.editArray();
- initArgs.nOptions = mOptions.size();
- initArgs.ignoreUnrecognized = JNI_FALSE;
- // 启动dalvik虚拟机
- if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
- . . . . . .
- }
- . . . . . .
- }
3.1.3注册Android内部需要的函数,startReg()
当虚拟机成功启动后,JNI环境也就建立好了,现在可以把JNIEnv*传递给startReg()来注册一些重要的JNI接口了。startReg()的代码截选如下:
【frameworks/base/core/jni/AndroidRuntime.cpp】
- int AndroidRuntime::startReg(JNIEnv* env)
- {
- androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
- . . . . . .
- env->PushLocalFrame(200);
- if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
- env->PopLocalFrame(NULL);
- return -1;
- }
- env->PopLocalFrame(NULL);
- return 0;
- }
- 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) { // 回调每个RegJNIRec数组项的mProc
- #ifndef NDEBUG
- ALOGD("----------!!! %s failed to load\n", array[i].mName);
- #endif
- return -1;
- }
- }
- return 0;
- }
- #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
- static const RegJNIRec gRegJNI[] = {
- REG_JNI(register_android_debug_JNITest),
- REG_JNI(register_com_android_internal_os_RuntimeInit), // 举例
- REG_JNI(register_android_os_SystemClock),
- REG_JNI(register_android_util_EventLog),
- REG_JNI(register_android_util_Log),
- REG_JNI(register_android_util_FloatMath),
- REG_JNI(register_android_text_format_Time),
- REG_JNI(register_android_content_AssetManager),
- . . . . . .
- . . . . . .
【frameworks/base/core/jni/AndroidRuntime.cpp】
- int register_com_android_internal_os_RuntimeInit(JNIEnv* env)
- {
- return jniRegisterNativeMethods(env, "com/android/internal/os/RuntimeInit",
- gMethods, NELEM(gMethods));
- }
- static 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 },
- };
3.1.4加载ZygoteInit类
AppRuntime的start()最后会加载Java层次的ZygoteInit类,并利用JNI技术的CallStaticVoidMethod()调用其静态的main()函数。
- jclass startClass = env->FindClass(slashClassName);
- . . . . . .
- jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
- "([Ljava/lang/String;)V");
- . . . . . .
- env->CallStaticVoidMethod(startClass, startMeth, strArray);
这是很关键的一步,就在这一步,控制权就转移到Java层次了。
3.2走入Java层——ZygoteInit.java
随着控制权传递到Java层次,ZygoteInit要做一些和Android平台紧密相关的重要动作,比如创建LocalServerSocket对象、预加载一些类以及资源、启动“Android系统服务”、进入核心循环等等。我们先画一张示意图:
相应地,我们还可以把前文的调用关系也丰富一下,得到下图:
3.2.1registerZygoteSocket()
我们先看ZygoteInit的main()函数调用的那个registerZygoteSocket()。这个函数内部其实会利用一个叫作“ANDROID_SOCKET_zygote”的环境变量。可是这个环境变量又是从哪里来的呢?为了解答这个问题,我们需要先看一下init进程service_start()函数。
3.2.1.1先看一下init进程的service_start()
前文我们已经列出了在init.rc脚本中,zygote服务是如何声明的。现在我们只关心其中和socket相关的部分:
- service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
- . . . . . .
- socket zygote stream 660 root system
- . . . . . .
当init进程真的启动zygote服务时,会走到service_start()。我们现在只关心service_start()里和socket相关的动作。
【system/core/init/Init.c】
- void service_start(struct service *svc, const char *dynamic_args)
- {
- . . . . . .
- pid = fork(); // 先fork出新的service进程
- if (pid == 0)
- {
- struct socketinfo *si;
- struct svcenvinfo *ei;
- . . . . . .
- // 再为service进程创建必须的socket接口
- for (si = svc->sockets; si; si = si->next)
- {
- int socket_type = (
- !strcmp(si->type, "stream") ? SOCK_STREAM :
- (!strcmp(si->type, "dgram") ? SOCK_DGRAM : SOCK_SEQPACKET));
- int s = create_socket(si->name, socket_type,
- si->perm, si->uid, si->gid);
- if (s >= 0) {
- // 将socket接口记入ANDROID_SOCKET_zygote环境变量
- publish_socket(si->name, s);
- }
- }
- . . . . . .
- if (execve(svc->args[0], (char**) svc->args, (char**) ENV) < 0) {
- ERROR("cannot execve('%s'): %s\n", svc->args[0], strerror(errno));
- }
- }
- . . . . . .
- }
create_socket()会在/dev/socket目录中创建一个Unix范畴的socket,而后,publish_socket()会把新建的socket的文件描述符记录在以“ANDROID_SOCKET_”打头的环境变量中。比如zygote对应的socket选项中的socket名为“zygote”,那么该socket对应的环境变量名就是“ANDROID_SOCKET_zygote”。
create_socket()的代码截选如下:
【system/core/init/Util.c】
- int create_socket(const char *name, int type, mode_t perm, uid_t uid, gid_t gid)
- {
- struct sockaddr_un addr;
- int fd, ret;
- . . . . . .
- fd = socket(PF_UNIX, type, 0);
- . . . . . .
- // "/dev/socket/zygote"
- snprintf(addr.sun_path, sizeof(addr.sun_path), ANDROID_SOCKET_DIR"/%s", name);
- ret = unlink(addr.sun_path);
- . . . . . .
- ret = bind(fd, (struct sockaddr *) &addr, sizeof (addr)); // 给套接字命名
- . . . . . .
- chown(addr.sun_path, uid, gid);
- chmod(addr.sun_path, perm);
- . . . . . .
- return fd;
- . . . . . .
- }
接下来,service_start()还调用了个publish_socket()函数,该函数的代码如下:
【system/core/init/Init.c】
- static void publish_socket(const char *name, int fd)
- {
- char key[64] = ANDROID_SOCKET_ENV_PREFIX;
- char val[64];
- strlcpy(key + sizeof(ANDROID_SOCKET_ENV_PREFIX) - 1,
- name,
- sizeof(key) - sizeof(ANDROID_SOCKET_ENV_PREFIX));
- snprintf(val, sizeof(val), "%d", fd); // 将文件描述符转为字符串
- add_environment(key, val);
- /* make sure we don't close-on-exec */
- fcntl(fd, F_SETFD, 0);
- }
上面代码中的ANDROID_SOCKET_ENV_PREFIX的定义如下:
- #define ANDROID_SOCKET_ENV_PREFIX "ANDROID_SOCKET_"
add_environment()的代码如下:
- int add_environment(const char *key, const char *val)
- {
- int n;
- for (n = 0; n < 31; n++) {
- if (!ENV[n]) {
- size_t len = strlen(key) + strlen(val) + 2;
- char *entry = malloc(len);
- snprintf(entry, len, "%s=%s", key, val);
- ENV[n] = entry;
- return 0;
- }
- }
- return 1;
- }
3.2.1.2registerZygoteSocket()里创建LocalServerSocket
OK,我们已经看到init进程在新fork出的zygote进程里,是如何记录“ANDROID_SOCKET_zygote”环境变量的。现在我们可以回过头来看zygote中的registerZygoteSocket()了,此处会切实地用到这个环境变量。
registerZygoteSocket()的代码如下:
【frameworks/base/core/java/com/android/internal/os/ZygoteInit.java】
- private static void registerZygoteSocket() {
- if (sServerSocket == null) {
- int fileDesc;
- try {
- String env = System.getenv(ANDROID_SOCKET_ENV);
- fileDesc = Integer.parseInt(env); // 从环境变量的字符串中解析出文件描述符
- } catch (RuntimeException ex) {
- throw new RuntimeException(
- ANDROID_SOCKET_ENV + " unset or invalid", ex);
- }
- try {
- sServerSocket = new LocalServerSocket(createFileDescriptor(fileDesc));
- } catch (IOException ex) {
- throw new RuntimeException(
- "Error binding to local socket '" + fileDesc + "'", ex);
- }
- }
- }
现在我们可以画一张创建zygote socket接口的示意图,如下:
请注意,图中明确画出了两个进程,一个add环境变量,另一个get环境变量。
3.2.2预加载一些类——preloadClasses()
注册完socket接口,ZygoteInit会预加载一些类,这些类记录在frameworks/base/preloaded-classes文本文件里。下面是该文件的一部分截选:
- # Classes which are preloaded by com.android.internal.os.ZygoteInit.
- # Automatically generated by frameworks/base/tools/preload/WritePreloadedClassFile.java.
- # MIN_LOAD_TIME_MICROS=1250
- # MIN_PROCESSES=10
- android.R$styleable
- android.accounts.Account
- android.accounts.Account$1
- android.accounts.AccountManager
- android.accounts.AccountManager$12
- android.accounts.AccountManager$13
- android.accounts.AccountManager$6
- android.accounts.AccountManager$AmsTask
- android.accounts.AccountManager$AmsTask$1
- android.accounts.AccountManager$AmsTask$Response
- . . . . . .
- . . . . . .
preloadClasses()的代码截选如下:
- private static void preloadClasses()
- {
- . . . . . .
- InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(
- PRELOADED_CLASSES); // 即"preloaded-classes"
- . . . . . .
- . . . . . .
- try {
- BufferedReader br = new BufferedReader(new InputStreamReader(is), 256);
- . . . . . .
- while ((line = br.readLine()) != null)
- {
- line = line.trim();
- . . . . . .
- Class.forName(line); // 使用加载当前类的类加载器来加载指定类
- . . . . . .
- count++;
- . . . . . .
- }
- . . . . . .
- } catch (IOException e) {
- . . . . . .
- } finally {
- . . . . . .
- runtime.preloadDexCaches();
- . . . . . .
- }
- . . . . . .
- }
3.2.3预加载一些系统资源——preloadResources()
除了预加载一些类,zygote进程还要预加载一些系统资源。
- private static void preloadResources()
- {
- . . . . . .
- mResources = Resources.getSystem();
- mResources.startPreloading();
- if (PRELOAD_RESOURCES) {
- . . . . . .
- TypedArray ar = mResources.obtainTypedArray(
- com.android.internal.R.array.preloaded_drawables);
- int N = preloadDrawables(runtime, ar);
- ar.recycle();
- . . . . . .
- ar = mResources.obtainTypedArray(
- com.android.internal.R.array.preloaded_color_state_lists);
- N = preloadColorStateLists(runtime, ar);
- ar.recycle();
- . . . . . .
- }
- mResources.finishPreloading();
- . . . . . .
- }
加载第一类资源需要调用preloadDrawables(),逐个加载TypedArray里记录的图片资源:基本上有两大类资源:
1)一类和图片有关(preloaed_drawables)
2)另一类和颜色有关(preloaded_color_state_lists)
- private static int preloadDrawables(VMRuntime runtime, TypedArray ar)
- {
- int N = ar.length();
- for (int i=0; i<N; i++) {
- . . . . . .
- int id = ar.getResourceId(i, 0); // 获得i项对应的资源id
- . . . . . .
- if (id != 0) {
- if (mResources.getDrawable(id) == null) {
- throw new IllegalArgumentException(
- "Unable to find preloaded drawable resource #0x"
- + Integer.toHexString(id)
- + " (" + ar.getString(i) + ")");
- }
- }
- }
- return N;
- }
其中的mResources是ZygoteInit的私有静态成员:
- private static Resources mResources;
另一些资源是颜色资源,是用preloadColorStateLists()加载的:
- private static int preloadColorStateLists(VMRuntime runtime, TypedArray ar) {
- int N = ar.length();
- for (int i=0; i<N; i++) {
- . . . . . .
- int id = ar.getResourceId(i, 0);
- . . . . . .
- if (id != 0) {
- if (mResources.getColorStateList(id) == null) {
- throw new IllegalArgumentException(
- "Unable to find preloaded color resource #0x"
- + Integer.toHexString(id)
- + " (" + ar.getString(i) + ")");
- }
- }
- }
- return N;
- }
<item>@color/primary_text_dark</item>
这个颜色集的参考文件是frameworks/base/core/res/res/color/primary_text_dark.xml,
现在,我们画一张加载系统资源的调用关系图:
转自http://blog.csdn.net/codefly/article/details/48403529
Android4.4的zygote进程(上)的更多相关文章
- Android4.4的zygote进程(下)
3.2.4启动Android系统服务——startSystemServer() 接下来就是启动Android的重头戏了,此时ZygoteInit的main()函数会调用startSystemServe ...
- Android4.4 Framework分析——Zygote进程的启动过程
Android启动过程中的第一个进程init.在启动过程中会启动两个关键的系统服务进程ServiceManager和Zygote. 本文要介绍的就是Zygote进程的启动,Zygote俗称孵化器,专门 ...
- Zygote进程【1】——Zygote的诞生
在Android中存在着C和Java两个完全不同的世界,前者直接建立在Linux的基础上,后者直接建立在JVM的基础上.zygote的中文名字为"受精卵",这个名字很好的诠释了zy ...
- Zygote进程【3】——SystemServer的诞生
在ZygoteInit的main()方法中做了几件大事,其中一件便是启动Systemserver进程,代码如下: @/frameworks/base/core/Java/com/Android/int ...
- Android系统启动流程(二)解析Zygote进程启动过程
1.Zygote简介 在Android系统中,DVM(Dalvik虚拟机).应用程序进程以及运行系统的关键服务的SystemServer进程都是由Zygote进程来创建的,我们也将它称为孵化器.它通过 ...
- Android Zygote进程是如何fork一个APP进程的
进程创建流程 不管从桌面启动应用还是应用内启动其它应用,如果这个应用所在进程不存在的话,都需要发起进程通过Binder机制告诉system server进程的AMS system server进程的A ...
- Android Zygote进程启动分析
dvm,app进程,linux进程三者关系 DVM指 dalivk 的虚拟机.每一个 Android 应用程序都在它自己的进程中运行,都拥有一个独立的 Dalvik 虚拟机实例.而每一个 DVM 都是 ...
- Zygote进程介绍【转】
本文转载自:http://blog.csdn.net/yangwen123/article/details/17258023 Zygote进程介绍 在Android系统中,存在不同的服务,这些服务 ...
- Zygote进程【2】——Zygote的分裂
在Zygote的诞生一文中init进程是如何一步步创建Zygote进程的,也了解了Zygote的进程的作用.Zygote进程的诞生对于整个Java世界可以说有着"开天辟地"的作用, ...
随机推荐
- threadlocal彻底理解,深刻
本文转自http://blog.csdn.net/huachao1001/article/details/51970237 ThreadLocal的使用相信大家都比较熟悉,但是ThreadLocal内 ...
- 【shiro】使用shiro,点击页面请求总是302状态码
解决方法: 配置shiro中,将要求放过的地址后面加上后缀,这里是.htmls 因为web.xml中配置所有的页面都是放过的
- php判断手机客户端
<?php // check if wap function check_wap(){ if(stristr($_SERVER['HTTP_VIA'],"wap")){// ...
- Sata win7 热插拔(AHCI)
主板支持AHCI,把sata模式改成AHCI,在bios打开SATA热插拔开关 开启AHCI,需要修改注册表:HKEY_LOCAL_MACHINE\System\CurrentControlSet\S ...
- (原)未能启用约束。一行或多行中包含违反非空、唯一或外键约束的值与DATEADD
SQLServer2014,查询分析器中 这样的脚本是没有问题的:AND TPO.CREATEON <= DATEADD(DAY, 1, '2017/3/3 0:00:00') 但.NET D ...
- ONVIF-WSDL
http://www.onvif.org/onvif/ver10/device/wsdl/devicemgmt.wsdl http://www.onvif.org/onvif/ver10/event/ ...
- nginx最大并发连接数的思考:worker_processes、worker_connections、worker_rlimit_nofile
参考nginx官网:http://nginx.org/en/docs/ngx_core_module.html#worker_connections 从用户的角度,http 1.1协议下,由于浏览器默 ...
- 模拟源码深入理解Vue数据驱动原理(1)
Vue有一核心就是数据驱动(Data Driven),允许我们采用简洁的模板语法来声明式的将数据渲染进DOM,且数据与DOM是绑定在一起的,这样当我们改变Vue实例的数据时,对应的DOM元素也就会改变 ...
- Hibernate原生SQL查询数据转换为HQL查询数据方法
HQL形式:(构造方法不支持timestamp类型) public List<Device> queryByMatherBoardId(String matherBoardId) { St ...
- 线程局部存储(TLS)
线程局部存储(TLS) 2011-10-11 09:59:28| 分类: Win32---API | 标签:tls |举报 |字号 订阅 什么是线程局部存储 众所周知,线程是执行的单元,同 ...