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

一 进程是什么?

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
通过上面两个命令可以确认几个常见进程的关系

进程名称pidppidtgidpgidsid
init10110
kthreadd20200
zygote6411111111111110
zygote11121111211120
system_server17351111173511110
com.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,第二次返回子进程的 id
  • count 是全局变量,子进程和父进程同时操作,但是互相不受影响

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

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

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值