Android系统启动系列1 进程基础

    科技2022-07-11  101

    一 进程是什么?

    1.1 什么是进程?

    可以认为进程是一个程序的一次执行过程,或者说一个正在执行的程序。在这个过程中,伴随着资源的分配和释放。进程是资源管理的最小单元。

    "进程四要素"--《Linux 内核源代码情景分析》描述如下:

    有一段程序供其执行拥有专用的系统堆栈空间在内核存在对应进程控制块拥有独立的用户存储空间

    1.2 进程与程序的区别与联系?

    程序是存放在硬盘中的二进制文件(可执行指令的序列),是静态的。而进程是已经加载到主存中,正在执行的程序。它是程序执行的过程,包括创建、调度和消亡。在这个过程中,伴随着资源的分配和释放,是动态的。

    1.3 进程在系统中的体现?

    1.3.1 可执行文件(elf) 文件的格式

    elf 文件中包括很多个字段,当程序执行时需要根据字段信息将程序加载到主存中。其中最重要的两个字段为:

    代码段(text)(即可执行的指令)数据段(data)(运算的中间数据)

    1.3.2 进程的内存分布

    代码区 程序(函数)代码所在,由编译而得到的二进制代码被载入至此.代码区是只读的,有执行权限.需要注意的是,字符串字面值(如"Hello World")就存储在这个区数据段和BSS段 合称静态区(全局区),用来存储静态(全局)变量.区别是前者(数据段)存储的是已初始化的静态(全局)变量,可读写;后者(BSS段)存储的是未初始化的静态(全局)变量,可读写堆 自由存储区.不像全局变量和局部变量的生命周期被严格定义,堆区的内存分配和释放是由程序员所控制的栈 由系统自动分配和释放,存储局部(自动)变量.

    1.3.3 进程在系统中的体现

    如上述,一个程序,实际上的格式为一个 elf 文件,当执行该程序时,首先按照 elf 文件的各个段,将该程序从硬盘按照上述图表所示加载到主存中,程序存在于一个进程的用户空间,而在内核空间中,维护了一个很大的结构,用于标识该进程的执行的状态。这个结构的名字叫 task_struct,task_struct 又称进程控制块,task_struct 定义在 include/linux/sched.h 文件中摘要如下:

    struct task_struct { volatile long state;//进程的状态信息 void *stack; atomic_t usage; unsigned int flags; /* per process flags, defined below */ pid_t pid; //进程的pid是进程的唯一标识 pid_t tgid;//线程组的id //如果创建它的父进程不再存在,则指向PID为1的init进程 struct task_struct *real_parent; //parent process “养父进程”通常与real_parent值相同 //当它终止时,必须向它的父进程发送信号 struct task_struct *parent; struct list_head children;//该进程的孩子进程链表 struct list_head sibling;//该进程的兄弟进程链表 struct list_head thread_group; //*线程链表 struct task_struct *group_leader;//该进程的线程组长 struct timespec start_time; //进程创建时间 struct fs_struct *fs; //它包含此进程当前工作目录和根目录 //打开的文件相关信息结构体。f_mode字段描述该文件是以什么模式创建的: //只读、读写、还是只写。f_pos保存文件中下一个读或写将发生的位置 struct files_struct *files; //描述进程的内存使用情况,active_mm指向进程运行时所使用的内存描述符对于普通进程而言 //这两个指针变量的值相同,但是内核线程kernel thread是没有进程地址空间的 struct mm_struct *mm, *active_mm; //static_prio用于保存静态优先级,prio用于保存动态优先级 int prio, static_prio, normal_prio; unsigned int rt_priority;//表示进程的运行优先级 /* signal handlers */ struct signal_struct *signal; struct sighand_struct *sighand; }

    POSIX(Portable Operating System Interface for Computing System,准确地说是针对类 Unix 操作系统的标准化协议)规定一个进程内部的多个 thread 要共享一个 PID,在很多情况下,进程都是动态分配一个 task_struct 表示,其实线程也是由一个task_struct 来表示的,所以 task_struct 具有双重身份,既可以作为进程对象,也可以作线程对象。这样,为了满足 POSIX 的线程规定,Linux 引入了线程组的概念,一个进程中的所有线程所共享的那 个PID 被称为线程组 ID,也就是 task struct 中的 tgid 成员,因此,在 Linux kernel 中,线程组 ID(tgid,thread group id)就是传统意义的进程 ID。对于 getpid 系统调用,Linux 内核返回了 tgid。对于 gettid 系统调用,本意是要求返回线程 ID,在 Linux 内核中,返回了 task struct 的 pid 成员。简单来一句总结:POSIX 的进程 ID 就是 Linux 中的线程组 ID。POSIX 的线程 ID 也就是 Linux 中的 pid,特别强调的是 task_struct 具有双重身份,线程和进程都是用 task_struct 表示,区别在于进程拥有独立的用户空间,而线程和其它线程是共享存储空间的。

    1.3.4 进程的状态

    task_struct 的 state 域表示进程的状态,有以下几种

    可运行态状态(R TASK-RUNNING) 此时进程正在运行,或者正在运行队列中等待调度。可中断睡眠态(S TASK_INTERRUPTIBLE) 此时进程正处于阻塞(睡眠)状态,正在等待某些事件发生或者能够炸弄某些资源。例如设备初始化完成、I/O 操作完成或定时器到时等。处在这种状态下的进程可以被信号中断。进程会在等待的事件发生或者是接收到信号被唤醒,进程转变为(TASK_RUNINNG状态)不可中断睡眠态(D TASK_UNINTERRUPTIBLE) 与TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。信号传递给这种状态下的进程不能改变它的状态。只有在它等待的事件发生时,进程才被显示的唤醒。暂停态(T TASK_STOPPED) 进程的执行被暂停,当进程收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号,就会进入暂停态。向进程发送一个SIGCONT信号,可以让其从TASK_STOPPED状态恢复到TASK_RUNNING状态僵尸态 (Z EXIT_ZOMBIE) 子进程运行结束,但是父进程尚未回收子进程的退出状态。处于这种状态下的进程已经放弃所有了几乎所有的资源,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,这个task_struct的空壳就形象地被称为僵尸。消亡态(X EXIT_DEAD) 父进程回收了子进程了退出状态后,进入的最终状态。这意味着接下来该进程将被彻底释放。所以EXIT_DEAD状态是非常短暂的,几乎不可能通过ps命令捕捉到。

    1.4 状态转换图

    1.5 进程的运行模式

    1.5.1 进程的运行状态

    进程的执行模式分为用户模式和内核模式,也称为用户态和内核态。处于用户模式的进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。 注意: 内核空间的是有内核负责映射的,它不会随着进程的改变,是固定的。内核空间地址有自己对应的页表,用户进程各自有不同的页表。每个进程的用户空间都是完全独立、互不相干。

    1.5.2 状态切换

    用户模式的进程只有通过使用系统调用,或者中断的方式,才能切换到内核模式,访问内核空间。

    1.5.3 状态切换举栗子

    系统调用其实也是通过中断实现的,系统调用的中断号为 0x80。每个中断会有中断处理函数,系统调用的中断处理函数为 systemcall(),func() 函数会将中断号和系统调用号一起传递给内核。内核根据系统调用号执行系统调用处理函数,例如 open() 函数最终调用的就是 system_open() 系统调用处理函数,然后返回一个文件描述符给用户。

    二 进程进阶

    2.1 pid ,ppid ,tgid ,pgid ,sid 的理解

    上面了解了进程的数据结构,我们可以通过下面两条命令来查看进程的信息,进一步加强进程相关标识的理解(pid ,ppid ,tgid ,pgid ,sid )

    cat /proc/self/status cat /proc/self/stat

    拿头条App举例

    jason:/ $ ps -ef |grep com.ss.android.article.news u0_a159 10276 1112 87 15:19:50 ? 00:00:32 com.ss.android.article.news u0_a159 10731 1112 1 15:19:56 ? 00:00:00 com.ss.android.article.news:pushservice u0_a159 10794 1112 9 15:19:57 ? 00:00:02 com.ss.android.article.news:push u0_a159 10953 1112 2 15:19:58 ? 00:00:00 com.ss.android.article.news:ad shell 11198 11193 6 15:20:27 pts/0 00:00:00 grep com.ss.android.article.news jason:/ $ cat /proc/10276/stat 10276 (id.article.news) S 1112 1111 0 0 -1 1077936448 365859 144549 137 0 3783 3165 1004 406 16 -4 144 0 31842 2094268416 53986 18446744073709551615 1 1 0 0 0 0 4612 1 1073779960 0 0 0 17 2 0 0 0 0 0 0 0 0 0 0 0 0 0

    每个参数意思为: pid=10276 进程(包括轻量级进程,即线程)号 comm=id.article.news 应用程序或命令的名字 task_state=S 任务的状态,R:running, S:sleeping, D:disk T: stopped, T:tracing stop,Z:zombie, X:dead ppid=1112 父进程ID pgid=1111 Process Group ID 进程组 ID号 sid=0 该任务所在的会话组ID TODO:内存中进程是怎么组织的 …

    pgid是什么:每个进程都会属于一个进程组(process group),每个进程组中可以包含多个进程。进程组会有一个进程组领导进程 (process group leader),领导进程的PID成为进程组的ID (process group ID, PGID),以识别进程组。

    sid是什么:更进一步,在shell支持工作控制(job control)的前提下,多个进程组还可以构成一个会话 (session),sid标识会话id,Android中进程的sid基本都是0。

    130|jason:/ $ cat /proc/10276/status Name: id.article.news State: S (sleeping) Tgid: 10276 Pid: 10276 PPid: 1112 TracerPid: 0 Uid: 10159 10159 10159 10159 Gid: 10159 10159 10159 10159 Ngid: 0 FDSize: 512 Groups: 3002 3003 9997 20159 50159 VmPeak: 2078244 kB VmSize: 2042672 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 234364 kB VmRSS: 208068 kB VmData: 335236 kB VmStk: 8192 kB VmExe: 20 kB VmLib: 190804 kB VmPTE: 1584 kB VmPMD: 16 kB VmSwap: 0 kB Threads: 142

    Tgid是什么:

    对于一个多线程的进程来说,它实际上是一个进程组,每个线程在调用getpid()时获取到的是自己的tgid值,而线程组领头的那个领头线程的pid和tgid是相同的 对于独立进程,即没有使用线程的进程来说,它只有唯一一个线程,领头线程,所以它调用getpid()获取到的值就是它的pid 通过上面两个命令可以确认几个常见进程的关系

    进程名称pidppidtgidpgidsidinit10110kthreadd20200zygote6411111111111110zygote11121111211120system_server17351111173511110com.ss.android.article.news1027611121027611110

    用下面的图表示更直观

    1号进程: init进程,用户空间的第一个进程,也是所有用户态进程的始祖进程,负责创建和管理各个 native进程。也有0号线程,swapper 进程、又叫 idle 进程,它创建了 init 进程和 ktheadd 进程2号进程: kthreadd 进程,内核线程的始祖进程,负责创建 ksoftirqd/0 等内核线程zygote 进程 init 创建的,有64位和32位两种,所有的java进程都是由他们孵化而来,他们是所有 java 进程的父进程system_server进程 Android 的核心进程,1735号线程是其主线程com.ss.android.article.news 普通的一个32位 java 进程

    从表格中列举的关系,可以看到一个 Android 的 App 进程的创建过程,是由 idle 进程 -> init 进程 -> zygote 进程 -> system_serve r进程 -> App 进程。

    问题:64位下有两个 zygote,zygote64 和 zygote。64位应用的父进程是 zygote64,它的 pgid 也是 zygote64 的 pid;32位应用的父进程是 zygote,它的 pgid 却是 zygote64 的 pid,如:com.ss.android.article.news 的父进程是 zygote(1112),但它的 pgid 是 zygote64(1111),这是怎么回事呢?原来不管32位或64位的 zygote,它在创建完子进程后,会调用 setChildPgid() 来改变子进程的 pgid。

    private void setChildPgid(int pid) { // Try to move the new child into the peer's process group. try { Os.setpgid(pid, Os.getpgid(peer.getPid())); } catch (ErrnoException ex) { // This exception is expected in the case where // the peer is not in our session // TODO get rid of this log message in the case where // getsid(0) != getsid(peer.getPid()) Log.i(TAG, "Zygote: setpgid failed. This is " + "normal if peer is not in our session"); } }

    peer 是 socket 的对端,也就是 system_server。而 system_server 的 pgid 就是 zygote64 的 pid。这样,所有 zygote32 创建出来的子进程,他们的 pgid 都是 zygote64 的 pid 了。

    三 如何创建一个进程

    在linux中可以使用fork()来创建一个进程,来看下函数的定义以及返回值,函数原型 pid_t fork(void) 函数返回值: 0: 子进程 , -1: 出错, >0: 父进程

    #include <unistd.h> #include <stdio.h> #include <wait.h> int main() { int count = 0; pid_t fpid = fork(); if (fpid < 0) { printf("创建父子进程失败!"); } else if (fpid == 0) { printf("子进程ID:%d\n", getpid()); count++; } else { printf("父进程ID:%d\n", getpid()); count=10; } printf("count=%d\n", count); waitpid(fpid, NULL, 0); return 0; } /home/wangjing/CLionProjects/untitled/cmake-build-debug/untitled 父进程ID:15229 count=10 子进程ID:15230 count=1 Process finished with exit code 0

    通过打印的结果有两点重要信息需要知道:

    fork 函数执行一次,返回两次,第一次返回父进程的 id,第二次返回子进程的 idcount 是全局变量,子进程和父进程同时操作,但是互相不受影响

    利用 fork() 函数将整个程序分成了两半,在 pid_t fpid == 0 是子进程执行的分支,大于0则是父进程执行的分支。 count=0 这个变量被原封不动地拷贝到这两个分支之中。 一个进程调用 fork() 函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。 其实进程的 fork 基于写时复制技术,相对与传统 fork 技术更加高效。何为写时复制技术呢?

    内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。

    假设现在有一个进程p1,包括正文段(可重入的程序,能被若干进程共享,比如代码等),数据段(用于保存程序已经初始化的变量),堆,栈。也有文件描述符等。 可以看到传统的fork系统调用直接把父进程所有的资源复制给新创建的进程,如果这时子进程执行exec函数系统调用,那么这种复制毫无意义,在看写时复制技术。 fork()之后父进程的将自己的虚拟空间拷贝给子进程,使得子进程可以共享父进程的物理空间,节省了很多物理内存。等到子进程需要写的时候,内核会为子进程分配数据段,堆,栈等,而正文段段继续共享父进程的。很显然,基于写时复制,进程的创建会更加高效。

    Processed: 0.017, SQL: 8