精华内容
下载资源
问答
  • 进程的描述与控制 操 作 系 统 所 具 有 的 四 大 特 征 也 都 是 基 于 进 程 而 形 成 的 , ...图中的每个结点可用来表示一个进程或程序段,乃至一条语句,结点间的有向边则表示两个结点之间存在的偏序(Partial Orde

    操作系统中进程的描述与控制

    操 作 系 统 所 具 有 的 四 大 特 征 也 都 是 基 于 进 程 而 形 成 的 , 并 从 进 程 的 角 度 对 操 作 系 统 进 行 研 究 。

    (一)、前趋图和程序执行

    1.1 前趋图

    • 前趋图(Precedence Graph),是指一个有向无循环图,可记为DAG(Directed Acyclic Graph),它用于描述进程之间执行的先后顺序。图中的每个结点可用来表示一个进程或程序段,乃至一条语句,结点间的有向边则表示两个结点之间存在的偏序(Partial Order)或前趋关系(Precedence Relation)
    • 进程(或程序)之间的前趋关系可用“→”来表示,如果进程Pi和Pj存在着前趋关系,可表示为(Pi,Pj)∈→,也可写成Pi→Pj,表示在Pj开始执行之前Pi 必须完成。此时称Pi是Pj的直接前趋,而称Pj是Pi的直接后继。
    • ①、在前趋图中,把没有前趋的结点称为初始结点(Initial Node)
      ②、把没有后继的结点称为终止结点(Final Node)。
      ③、此外,每个结点还具有一个重量(Weight),用于表示该结点所含有的程序量或程序的执行时间。

    在这里插入图片描述

    在图(a)所示的前趋图中,存在着如下前趋关系:
    P1→P2,P1→P3,P1→P4,P2→P5,P3→P5,P4→P6,P4→P7,P5→P8,P6→P8,P7→P9,P8→P9
    或表示为:P={P1, P2, P3, P4, P5, P6, P7, P8, P9} ={(P1, P2), (P1, P3), (P1, P4), (P2, P5), (P3, P5), (P4, P6), (P4, P7), (P5, P8), (P6, P8), (P7, P9), (P8, P9)}

    • 应当注意,前趋图中是不允许有循环的,否则必然会产生不可能实现的前趋关系。
    • 如图(b)所示的前趋关系中就存在着循环。它一方面要求在S3开始执行之前,S2必须完成,另一方面又要求在S2开始执行之前,S3必须完成。显然,这种关系是不可能实现的。S2→S3,S3→S2
      在这里插入图片描述

    1.2 程序顺序执行

    (1)程序的顺序执行
    (2)程序顺序执行时的特征
    • ①、顺序性处理机的操作严格按照程序所规定的顺序执行,每一操作必须在下一个操作开始之前结束。

    • ②、封闭性在封闭环境下执行,独占全机资源,执行结果不受外界影响。

    • ③、可再现性只要程序执行时的环境和初始条件相同,当程序重复执行时,不论它是从头到尾不停顿地执行,还是“走走停停”地执行,都将获得相同的结果。

    1.3 程序并发执行

    (1)程序的并发执行

    在这里插入图片描述
    由图可以看出,存在前趋关系 Ii→Ci,Ii→Ii+1,Ci→Pi,Ci→Ci+1,Pi→Pi+1,而Ii+1和Ci及Pi-1是重叠的,即在Pi-1和Ci 以及 Ii+1之间,不存在前趋关系,可以并发执行。

    (2)程序并发执行时的特征
    • ①、间断性相互制约导致并发程序具有“执行-暂停-执行”这种间断性的活动规律。
    • ②、失去封闭性多个程序共享系统中的各种资源,资源状态由多个程序来改变。
    • ③、不可再现性由于程序的并发执行,打破了由另一程序独占系统资源的封闭性,因而破坏了可再现性。
      在这里插入图片描述

    (二)、进程的描述

    2.1 进程的定义和特征

    (1)进程的定义
    • 由程序段、相关的数据段和PCB三部分便构成了进程实体(又称进程映像),一般简称为进程。
    • 对于进程的定义,从不同的角度可以有 不同的定义,其中较典型的定义有:
      ①、进程是程序的一次执行。
      ②、进程是一个程序及其数据在处理机上顺序执行时所发生的活动。
      ③、进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
    • 进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
    (2)进程和程序的区别
    • 程序是指令的有序集合,其本身没有任何运行的含义,它是一个静态的概念。而进程是程序在处理机上的一次执行过程,它是一个动态概念。(例:书本,讲课)
    • 程序的存在是永久的。而进程则是有生命期的,它因创建而产生,因调度而执行,因得不到资源而暂停执行,因撤消而消亡。
    • 程序仅是指令的有序集合。而进程则由程序段、相关数据段、进程控制块(PCB) 组成。
    • 进程与程序之间不是一一对应。
      在这里插入图片描述
    (3)进程的特征动态性
    • 动态性
      ①、进程的实质是程序在处理机上的一次执行过程,因此是动态的。所以动态性是进程的最基本的特征。
      ②、同时动态性还表现在进程是有生命期的,它因创建而产生,因调度而执行,因得不到资源而暂停执行,因撤消而消亡。

    • 并发性
      ①、指多个进程实体同时存在于内存中,能在一段时间内同时运行。
      ②、引入进程的目的就是为了使进程能并发执行,以提高资源利用率,所以并发性是进程的重要特征,也是OS的重要特征。

    • 独立性:
      指进程是一个能独立运行的基本单位,也是系统进行资源分配和调度的独立单位。

    • 异步性:
      指进程以各自独立的、不可预知的速度向前推进。

    • 程序是指令的集合。但进程不是

    2.2 进程的基本状态及转换

    (1)进程的三种基本状态

    由于多个进程在并发执行时共享系统资源,致使它们在运行过程中呈现间断性的运行规律,所以进程在其生命周期内可能具有多种状态。一般而言,每一个进程至少应处于以下三种基本状态之一:

    • ①、就绪(Ready)状态。这是指进程已处于准备好运行的状态,即进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行。
    • ②、执行(Running)状态。这是指进程已获得CPU,其程序正在执行的状态。
    • ③、阻塞(Block)状态。 这是指正在执行的进程由于发生某事件(如I/O请求、申请缓冲区失败等)暂时无法继续执行时的状态,亦即进程的执行受到阻塞。
    (2)三种基本状态的转换

    在这里插入图片描述

    • 可能导致一个进程从运行状态变为就绪状态的事件是出现了比现在进程优先级更高的进程
    • 一个进程从运行状态变为就绪状态必会引起进程切换
    • 在一个多道系统中,若就绪队列不空,就绪的进程数目多,处理器的效率不变
    (3)创建状态和终止状态创建状态
    • ①、创建状态:

      • 如首先由进程申请一个空白PCB,并向PCB中填写用于控制和管理进程的信息;然后为该进程分配运行时所必须的资源;最后,把该进程转入就绪状态并插入就绪队列之中。
      • 但如果进程所需的资源尚不能得到满足,比如系统尚无足够的内存使进程无法装入其中,此时创建工作尚未完成,进程不能被调度运行,于是把此时进程所处的状态称为创建状态
    • ②、终止状态

      • 进程的终止也要通过两个步骤:首先,是等待操作系统进行善后处理,最后将其PCB清零,并将PCB空间返还系统。
      • 当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结,它将进入终止状态。
      • 进入终止态的进程以后不能再执行,但在操作系统中依然保留一个记录,其中保存状态码和一些计时统计数据,供其他进程收集。一旦其他进程完成了对其信息的提取之后,操作系统将删除该进程,即将其PCB清零,并将该空白PCB返还系统。
        在这里插入图片描述
    • ③、挂起操作

      • 当该操作作用于某个进程时,该进程被挂起,意味着此时该进程处于静止状态;如果该进程处于就绪状态,则该进程此时暂不接受调度。与挂起操作对应的操作是激活操作。

    2.3 挂起操作和进程状态的转换

    (1)挂起操作的引入
    • 终端用户的需要:终端用户在自己程序运行中发现问题要求使正在执行的进程暂停执行而使进程处于挂起状态。
    • 父进程的需要:父进程为了考查和修改某个子进程,或者协调各子进程间的活动,需要将该子进程挂起。
    • 负荷调节的需要:由于工作负荷较重,而将一些不重要的进程挂起,以保证系统能正常运行(实时操作系统)
    • 操作系统的需要:操作系统为了检查运行中的资源使用情况或进行记帐,而将某些进程挂起
    (2)引入挂起原语操作后三个进程状态的转换
    • 活动就绪→静止就绪;静止就绪→活动就绪
      进程处于未被挂起的就绪状态就是活动就绪状态,此时进程可以接受调度。
      当使用挂起原语Suspend将该进程挂起后,该进程转变为静止就绪状态,此时进程不再被调度执行。
    • 活动阻塞→静止阻塞。静止阻塞→活动阻塞。
      当进程处于未被挂起的阻塞状态时,称为活动阻塞状态;
      当用Suspend原语 将活动阻塞状态挂起后,进程转变为静止阻塞状态,处于该状态的进程在其所期待的事件出现后,将从静止阻塞状态转变为静止就绪状态
      在这里插入图片描述
    (3)引入挂起操作后五个进程状态的转换
    • NULL→创建:新进程产生时,该进程处于创建状态
    • 创建→活动就绪:在当前系统的性能和内存的容量均允许的情况下,完成对进程创建的必要操作后,相应的系统进程将进程的状态转换为活动就绪状态
    • 创建→静止就绪:考虑到系统当前资源状况和性能的要求,不分配给新建进程所需资源,主要是主存,相应的系统将进程状态转为静止就绪状态
    • 执行→终止:当一个进程已完成任务时,或是出现了无法克服的错误,或是被OS或是被其他进程所终结,此时将进程的状态转换为终止状态
    注意:
    • 当进行处于就绪状态的情况下,可以被处理器调度执行。
    • 在任何时刻,一个进程的状态变化不一定引起另一个进程的状态变化。

    2.4 进程管理中的数据结果

    (1)操作系统中用于管理控制的数据结构
    • 在计算机系统中,对于每个资源和每个进程都设置了一个数据结构,用于表征其实体,我们称之为资源信息表或进程信息表,其中包含了资源或进程的标识、描述、状态等信息以及一批指针通过这些指针,可以将同类资源或进程的信息表,或者同一进程所占用的资源信息表分类链接成不同的队列,便于操作系统进行查找。
    • OS管理的这些数据结构一般分为以下四类:内存表、设备表、文件表和用于进程管理的进程表,通常进程表又被称为进程控制块PCB。
    (2)进程控制块PCB的作用
    • ①、作为独立运行基本单位的标志。
      当一个程序配置了PCB后,就表示它已是一个能在多道程序环境下独立运行的合法的基本单位,也就是具有获得CPU的权利。
      当进程创建时就为它建立PCB,进程结束时又回收其PCB,进程也随之消亡。系统是通过PCB感知进程的存在的。

    • ②、能实现间断性运行方式。
      在多道程序环境下,程序是采用停停走走间断性的运行方式运行的。当进程因阻塞而暂停运行时,系统就可将CPU现场信息保存在被中断进程的PCB中,供该进程再次被调用执行时恢复CPU现场使用

    • ③、提供进程管理所需要的信息。
      当调度程序调度到某进程运行时,只能根据该进程PCB中记录的程序和数据在内存或外存中的起始指针,找到相应的程序和数据;
      在进程运行过程中,当需要访问文件系统中的文件或I/O设备时,也需要借助于PCB中的信息。
      可根据PCB中的资源清单了解到该进程所需的全部资源

    • ④、提供进程调度所需要的信息。
      在PCB中提供了进程处于何种状态的信息:包括进程的优先级、进程的等待时间和已执行时间等。

    • ⑤、实现与其它进程的同步与通信
      在采用信号量机制时,它要求在每个进程中都设置有相应的同步的信号量。在PCB中还具有用于实现进程通信的区域或通信队列指针等。

    注意:
    • 操作系统是根据进程控制块来对并发执行的进程进行控制和管理的
    (3)进程控制块中的信息
    • ①、进程标识符
      • 进程标识符用于唯一地标识一个进程。一个进程通常有两种标识符:
        1.外部标识符。由创建者提供,通常由字母数字组成,为了描述进程的家族关系,还应设置父进程标识及子进程标识,还可设置用户标识指示拥有该进程的用户
        2.内部标识符。赋予每一个进程一个唯一的数字标识符,它通常是一个进程的序号。
    • ②、处理机状态
      • 处理机状态信息也称为处理机的上下文,主要是由处理机的各种寄存器中的内容组成的。这些寄存器包括
        1.通用寄存器:又称为用户可视寄存器,它们是用户程序可以访问的,用于暂存信息。
        2.指令计数器:存放下一条指令的地址
        3.程序状态字PSW:状态信息等。
        4.用户栈指针:指向栈顶
    • ③、进程调度信息
      在OS进行调度时,必须了解进程的状态及有关进程调度的信息,这些信息包括:
      1.进程状态,指明进程的当前状态,它是作为进程调度和对换时的依据;
      2.进程优先级,是用于描述进程使用处理机的优先级别的一个整数,优先级高的进程应优先获得处理机;
      3.进程调度所需的其它信息,它们与所采用的进程调度算法有关,比如,进程已等待CPU的时间总和、进程已执行的时间总和等;
      4.事件,是指进程由执行状态转变为阻塞状态所等待发生的事件,即阻塞原因
    • ④、进程控制信息
      是指用于进程控制所必须的信息,它包括:
      1.程序和数据的地址,进程实体中的程序和数据的内存或外存地(首)址,以便再调度到该进程执行时,能从PCB中找到其程序和数据;
      2.进程同步和通信机制,这是实现进程同步和进程通信时必需的机制,如消息队列指针、信号量等,它们可能全部或部分地放在PCB中;
      3.资源清单,在该清单中列出了进程在运行期间所需的全部资源(除CPU以外),另外还有一张已分配到该进程的资源的清单;
      4.链接指针,它给出了本进程(PCB)所在队列中的下一个进程的PCB的首地址。
    (4)进程控制块的组织方式
    • 在一个系统中,通常可拥有数十个、数百个乃至数千个PCB。为了能对它们加以有效的管理,应该用适当的方式将这些PCB组织起来。目前常用的组织方式有三种:线性方式、链接方式、索引方式。
      • ①、线性方式,即将系统中所有的PCB都组织在一张线性表中,将该表的首址存放在内存的一个专用区域中。该方式实现简单、开销小,但每次查找时都需要扫描整张表,因此适合进程数目不多的系统。图示出了线性表的PCB组织方式。
        在这里插入图片描述
      • ②、链接方式,即把具有相同状态进程的PCB分别通过PCB中的链接字链接成一个队列。这样,可以形成就绪队列、若干个阻塞队列和空白队列等。
      • 对就绪队列而言,往往按进程的优先级将PCB从高到低进行排列,将优先级高的进程PCB排在队列的前面
      • 同样,也可把处于阻塞状态进程的PCB根据其阻塞原因的不同,排成多个阻塞队列,如等待I/O操作完成的队列和等待分配内存的队列等。
        在这里插入图片描述
      • ③、索引方式即系统根据所有进程状态的不同,建立几张索引表,例如,就绪索引表、阻塞索引表等,并把各索引表在内存的首地址记录在内存的一些专用单元中。
      • 在每个索引表的表目中,记录具有相应状态的某个PCB在PCB表中的地址。
        在这里插入图片描述

    (三)、进程控制

    • 进程控制是进程管理中最基本的功能,主要包括创建新进程、终止已完成的进程、将因发生异常情况而无法继续运行的进程置于阻塞状态、负责进程运行中的状态转换等功能。
    • 进程控制一般是由OS的内核中的原语来实现的
    (1)操作系统的内核
    • 将它们常驻内存,即通常被称为的OS内核。
    • 这种安排方式的目的在于两方面:一是便于对这些软件进行保护,防止遭受其他应用程序的破坏;二是可以提高OS的运行效率。
    • 相对应的是,为了防止OS本身及关键数据(如PCB等)遭受到应用程序有意或无意的破坏,通常也将处理机的执行状态分成系统态和用户态两种:
      • 系统态:又称为管态,也称为内核态。它具有较高的特权,能执行一切命令,访问所有寄存器和存储区,传统的OS都在系统态运行。
      • 用户态:又称为目态。它是具有较低特权的执行状态,仅能执行规定的指令,访问指定的寄存器和存储区。一般情况下,应用程序只能在用户态运行,不能去执行OS指令及访问OS区域,这样可以防止应用程序对OS的破坏。
    • (1)支撑功能
      该功能是提供给OS其它众多模块所需要的一些基本功能,以便支撑这些模块工作。其中三种最基本的支撑功能是:中断处理。时钟管理。原语操作
      • 进程创建和控制等都是由OS的内核中的原语来实现的
      • 原语:是由若干条指令组成的,用于完成一定功能的一个过程。
        它们是原子的操作。所谓原子的操作是指一个操作中的所有动作要么全做,要么全不做。
        原语是一个不可分割的基本单位,在执行过程中,不允许被中断
        原子操作在管态下执行,常驻内存。
    • (2)资源管理功能:①、进程管理。②、存储器管理。③、设备管理。
    (2)进程的创建
    • (1)进程的层次结构

      • 子进程可以继承父进程所有的资源。当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程。此外,再撤销父进程时,也必须同时撤销其所有的子进程
        在这里插入图片描述
      • 在windows中不存在任何进程层次结构的概念,所有的进程都具有相同的地位。
    • (2)进程图

      • 可用一条由进程Pi指向进程Pj的有向边来描述它们之间的父子关系。创建父进程的进程称为祖先进程,这样便形成了一棵进程树,把树的根结点作为进程家族的祖先(Ancestor)。
        在这里插入图片描述
    • (3)引起创建进程的事件

      • ①、用户登录:在分时OS中,用户在终端键入登录命令后,如是合法用户,则系统为该终端创建一进程,并插入就绪队列。
      • ②、作业调度:在批处理OS中,当按某算法调度一作业进内存,系统为之分配必要资源,同时为该作业创建一进程,并插入就绪队列。
      • ③、提供服务:在程序运行中,若用户需某种服务,则系统创建一进程为用户提供服务,并插入就绪队列。例如:打印进程
      • ④、应用请求:在运行中,由于应用进程本身的需求,自己创建一进程,并插入就绪队列。
    • (4)进程的创建

      • 在系统中每当出现了创建新进程的请求后,OS便调用进程创建原语Creat按下述步骤创建一个新进程:
        ①、申请空白PCB,为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB。
        ②、为新进程分配其运行所需的资源,包括各种物理和逻辑资源,如内存、文件、I/O设备和CPU时间等。
        ③、初始化进程控制块(PCB)。
        ④、如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列。
        在这里插入图片描述
    注意:1. 进程的标识符和进程控制块的分配发生在进程的创建阶段
    2. 由调度程序为进程分配CPU的步骤不是创建进程所必须的。
    (3)进程的终止
    • (1)引起进程终止(Termination of Process)的事件
      • 正常结束,表示进程的任务已经完成,准备退出运行。

      • 异常结束,是指进程在运行时发生了某种异常事件,使程序无法继续运行。常见的异常事件有:越界错、保护错、非法指令、特权指令错、运行超时、等待超时、算术运算错、I/O故障等。
        越界错误:这是指程序所访问的存储区已越出该进程的区域。
        保护错:指进程试图去访问一个不允许访问的资源或文件,或者以不适当的方式进行访问。
        非法指令:指程序试图去执行一条不存在的指令。出现该错误的原因,可能是程序错误的转移到数据区,把数据当成了指令。
        特权指令错:是指用户进程试图去执行一条只允许OS执行的指令。
        运行超时:是指进程的执行时间超过了指定的最大值
        等待超时:是指进程等待某事件的时间超过了指定的最大值
        算术运算错:是指进程试图去执行一个被禁止的运算,例如被0除
        I/O故障:是指在I/O过程中发生了错误等

      • 外界干预,是指进程应外界的请求而终止运行。这些干预有:操作员或操作系统干预、父进程请求、因父进程终止等。
        操作员或操作系统干预。由于某种原因,例如,发生了死锁,由操作员或操作系统终止该进程
        父进程请求:由于父进程具有终止自己的任何子孙进程的权利,因而当父进程提出请求时,系统将终止该进程
        父进程终止。当父进程终止时,OS也将它的所有子孙进程终止

    • (2)进程的终止过程
      如果系统中发生了要求终止进程的某事件,OS便调用进程终止原语,按下述过程去终止指定的进程:
      • ①、根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程的状态;
        ②、若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真,用于指示该进程被终止后应重新进行调度;
        ③、若该进程还有子孙进程,还应将其所有子孙进程也都予以终止,以防它们成为不可控的进程;
        ④、将被终止进程所拥有的全部资源或者归还给其父进程,或者归还给系统;
        ⑤、将被终止进程(PCB)从所在队列(或链表)中移出,等待其它程序来搜集信息。
        在这里插入图片描述
    (4)进程的阻塞与唤醒
    • (1)引起进程阻塞和唤醒的事件

      • ①、向系统请求共享资源失败。进程在向系统请求共享资源时,由于系统已无足够的资源分配给它,此时进程因不能继续运行而转变为阻塞状态。
        ②、等待某种操作的完成。当进程启动某种操作后,如果该进程必须在该操作完成之后才能继续执行,则应先将进程阻塞起来,以等待操作完成。
        ③、新数据尚未到达。对于相互合作的进程,如果一个进程需要先获得另一进程提供的数据后才能对该数据进行处理,只要其所需数据尚未到达,进程便只有阻塞。
        ④、等待新任务的到达。在某些系统中,特别是在网络环境下的OS,往往设置一些特定的系统进程,每当这种进程完成任务后便把自己阻塞起来,等待新任务的到来。
    • (2)进程阻塞过程

      • 正在执行的进程,如果发生了上述某事件,进程便通过调用阻塞原语block将自己阻塞。可见,阻塞是进程自身的一种主动行为
    • (3)进程唤醒过程

      • 当被阻塞进程所期待的事件发生时,比如它所启动的I/O操作已完成,或其所期待的数据已经到达,则由有关进程(比如提供数据的进程)调用唤醒原语wakeup,将等待该事件的进程唤醒。
      • wakeup执行的过程是:首先把被阻塞的进程从等待该事件的阻塞队列中移出,将其PCB中的现行状态由阻塞改为就绪,然后再将该PCB插入到就绪队列中。
        在这里插入图片描述
    注意:一个进程被唤醒,意味着该进程可以重新竞争CPU
    (5)进程的挂起与激活
    • (1)进程的挂起
      当系统中出现了引起进程挂起的事件时,OS将利用原语suspend将指定进程或处于阻塞状态的进程挂起。
      Suspend的执行过程是:
      首先检查被挂起进程的状态,若处于活动就绪状态,便将其改为静止就绪;
      对于活动阻塞状态的进程,则将之改为静止阻塞;

    为了方便用户或父进程考查该进程的运行情况,而把该进程的PCB复制到某指定的内存区域; 最后,若被挂起的进程正在执行,则转向调度程序重新调度。

    • (2)进程的激活过程
    • 当系统中发生激活过程的事件时,OS将利用激活原语active,将指定进程激活。
      激活原语先将进程从外存调入内存,检查该进程的现行状态,若是静止就绪,便将之改为活动就绪;若为静止阻塞,便将之改为活动阻塞。
    引入挂起状态的原因:

    ①、终端用户的需要:终端用户在自己程序运行中发现问题要求使正在执行的进程暂停执行而使进程处于挂起状态。
    ②、父进程的需要:父进程为了考查和修改某个子进程,或者协调各子进程间的活动,需要将该子进程挂起。
    ③、操作系统的需要:操作系统为了检查运行中的资源使用情况或进行记帐,而将某些进程挂起。
    ④、对换的需要:为了提高内存的利用率,将内存中某些进程挂起,以调进其它程序运行。
    ⑤、负荷调节的需要:由于工作负荷较重,而将一些不重要的进程挂起,以保证系统能正常运行(实时操作系统)
    在这里插入图片描述

    注意:当一个进程被终止、挂起、唤醒、阻塞时,可能会发生处理器的调度。

    为使进程由活动就绪转变为静止就绪,应利用 suspend原语;
    为使进程由执行状态转变为阻塞状态,应利用 block 原语;
    为使进程由静止就绪变为活动就绪,就应利用 active 原语;
    为使进程从阻塞状态变为就绪状态,应利用 wakeup 原语。

    展开全文
  • select_task_rq_fair 选核其实是一个优选的过程, 通常会有限选择一个 cache-miss 等开销最小的一个 根据 wake_affine 选择调度域并确定 new_cpu 根据调度域及其调度域参数选择兄弟 idle cpu 根据调度域及其调度...
    日期内核版本架构作者GitHubKernelShow
    2016-0729Linux-4.6X86 & armgatiemeLinuxDeviceDriversLinux进程管理与调度

    本文更新记录
    20200513 更新了【1.1 引入 WAKE_AFFINE 的背景】内容, 引入 WAKE_AFFINE 机制. 让大家对 WAKE_AFFINE 的目的有一个清楚认识.

    #1 wake_affine 机制

    ##1.1 引入 WAKE_AFFINE 的背景

    当进程被唤醒的时候(try_to_wake_up),需要用 select_task_rq_fair为该 task 选择一个合适的CPU(runqueue), 接着会通过 check_preempt_wakeup 去看被唤醒的进程是否要抢占所在 CPU 的当前进程.

    关于唤醒抢占的内容, 请参考 Linux唤醒抢占----Linux进程的管理与调度(二十三)

    调度器对之前 SLEEP 的进程唤醒后重新入 RUNQ 的时候, 会对进程做一些补偿, 请参考 Linux CFS调度器之唤醒补偿–Linux进程的管理与调度(三十)

    这个选核的过程我们一般称之为 BALANCE_WAKE. 为了能清楚的描述这个场景,我们定义

    • 执行唤醒的那个进程是 waker

    • 而被唤醒的进程是 wakee

    Wakeup有两种,一种是sync wakeup,另外一种是non-sync wakeup。

    • 所谓 sync wakeup 就是 waker 在唤醒 wakee 的时候就已经知道自己很快就进入 sleep 状态,而在调用 try_to_wake_up 的时候最好不要进行抢占,因为 waker 很快就主动发起调度了。此外,一般而言,waker和wakee会有一定的亲和性(例如它们通过share memory进行通信),在SMP场景下,waker和wakee调度在一个CPU上执行的时候往往可以获取较佳的性能。而如果在try_to_wake_up的时候就进行调度,这时候wakee往往会调度到系统中其他空闲的CPU上去。这时候,通过sync wakeup,我们往往可以避免不必要的CPU bouncing。

    • 对于non-sync wakeup而言,waker和wakee没有上面描述的同步关系,waker在唤醒wakee之后,它们之间是独立运作,因此在唤醒的时候就可以尝试去触发一次调度。

    当然,也不是说sync wakeup就一定不调度,假设waker在CPU A上唤醒wakee,而根据wakee进程的cpus_allowed成员发现它根本不能在CPU A上调度执行,那么管他sync不sync,这时候都需要去尝试调度(调用reschedule_idle函数),反正waker和wakee命中注定是天各一方(在不同的CPU上执行)。

    select_task_rq_fair 的原型如下:

    int select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
    

    在 try_to_wake_up 场景其中 p 是待唤醒进程, prev_cpu 是进程上次运行的 CPU, 一般 sd_flag 是 BALANCE_WAKE, 因此其实wakeup 的过程也可以理解为一次主动 BALANCE 的过程, 成为 WAKEUP BALANCE, 只不过只是为一个进程选择唤醒到的 CPU. wake_flags 用于表示是 sync wakeup 还是 non-sync wakeup.

    我们首先看看UP上的情况。这时候waker和wakee在同一个CPU上运行(当然系统中也只有一个CPU,哈哈),这时候谁能抢占CPU资源完全取决于waker和wakee的动态优先级(调度类优先级, 或者 CFS 的 vruntime 等, 依照进程的调度类而定),如果wakee的动态优先级大于waker,那么就标记waker的need_resched标志,并在调度点到来的时候调用schedule函数进行调度。

    SMP情况下,由于系统的CPU资源比较多,waker和wakee没有必要争个你死我活,wakee其实也可以选择去其他CPU执行,但是这时候要做决策:

    • 因为跑到 prev_cpu 上, 那么之前如果 cache 还是 hot 的是很有意义的

    • 同时按照之前的假设 waker 和 wakee 之间有资源共享, 那么唤醒到 waker CPU 上也有好处

    • 如果 prev_cpu, waker cpu 都很忙, 那放上来可以并不一定好, 唤醒延迟之类的都是一个考量.

    那么这些都是一个综合权衡的过程, 我们要考虑的东西比较多

    • wake_cpu,prev_cpu 到底该不该选择?

    • 选择的话选择哪个?

    • 它们都不合适的时候又要怎么去选择一个更合适的?

    内核需要一个简单有效的机制去做这个事情, 因此 WAKE_AFFINE 出现在内核中.

    ##1.2 WAKE_AFFINE 机制简介

    select_task_rq_fair 选核其实是一个优选的过程, 通常会有限选择一个 cache-miss 等开销最小的一个

    1. 根据 wake_affine 选择调度域并确定 new_cpu

    2. 根据调度域及其调度域参数选择兄弟 idle cpu 根据调度域及其调度域参数选择兄弟 idle cpu

    3. 根据调度域选择最深idle的cpu根据调度域选择最深idle的cpu find_idest_cpu

    在进程唤醒的过程中为进程选核时, wake_affine 倾向于将被唤醒进程尽可能安排在 waking CPU 上, 这样考虑的原因是: 有唤醒关系的进程是相互关联的, 尽可能地运行在具有 cache 共享的调度域中, 这样可以获得一些 chache-hit 带来的性能提升. 这时 wake_affine 的初衷, 但是这也是一把双刃剑.

    将 wakee 都唤醒在 waker CPU 上, 必然造成 waker 和 wakee 的资源竞争. 特别是对于 1:N 的任务模型, wake_affine 会导致 waker 进程饥饿.

    62470419e993f8d9d93db0effd3af4296ecb79a5 sched: Implement smarter wake-affine logic

    因此后来 (COMMIT 62470419e993 “sched: Implement smarter wake-affine logic”), 实现了一种智能 wake-affine 的优化机制. 用于 wake_flips 的巧妙方式, 识别出 1:N 等复杂唤醒模型, 只有在认为 wake_affine 能提升性能时(want_affine)才进行 wake_affine.

    #2 wake_affine 机制分析

    根据 want_affine 变量选择调度域并确定 new_cpu

    我们知道如下的事实 :

    • 进程p的调度域参数设置了SD_BALANCE_WAKE

    • 当前cpu的唤醒次数没有超标

    • 当前task p消耗的capacity * 1138小于min_cap * 1024

    • 当前cpu在task p的cpu亲和数里面的一个

    // https://elixir.bootlin.com/linux/v5.1.15/source/kernel/sched/fair.c#L6674
    static int
    select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
    {
     struct sched_domain *tmp, *sd = NULL;
     int cpu = smp_processor_id();
     int new_cpu = prev_cpu;
     int want_affine = 0;
     int sync = (wake_flags & WF_SYNC) && !(current->flags & PF_EXITING);
    
     if (sd_flag & SD_BALANCE_WAKE) {
      record_wakee(p);
    
      if (sched_energy_enabled()) {
       new_cpu = find_energy_efficient_cpu(p, prev_cpu);
       if (new_cpu >= 0)
        return new_cpu;
       new_cpu = prev_cpu;
      }
    
      want_affine = !wake_wide(p) && !wake_cap(p, cpu, prev_cpu) &&
             cpumask_test_cpu(cpu, &p->cpus_allowed);
     }
    
     rcu_read_lock();
     for_each_domain(cpu, tmp) {
      if (!(tmp->flags & SD_LOAD_BALANCE))
       break;
    
      /*
       * If both 'cpu' and 'prev_cpu' are part of this domain,
       * cpu is a valid SD_WAKE_AFFINE target.
       */
      if (want_affine && (tmp->flags & SD_WAKE_AFFINE) &&
          cpumask_test_cpu(prev_cpu, sched_domain_span(tmp))) {
       if (cpu != prev_cpu)
        new_cpu = wake_affine(tmp, p, cpu, prev_cpu, sync);
    
       sd = NULL; /* Prefer wake_affine over balance flags */
       break;
      }
    
      if (tmp->flags & sd_flag)
       sd = tmp;
      else if (!want_affine)
       break;
     }
    
    • wake_wide 和 wake_cap 为调度器提供决策, 当前进程是否符合 wake_affine 的决策模型. 如果他们返回 1, 则说明如果采用 wake_affine 进行决策, 大概率是无效的或者会降低性能, 则调度器就不会 want_affine 了.
    want_affine = !wake_wide(p) && !wake_cap(p, cpu, prev_cpu) &&
             cpumask_test_cpu(cpu, &p->cpus_allowed);
    

    wake_wide 检查当前cpu的唤醒关系符合 wake_affine 模型.
    wake_cap 检查当前 task p 消耗的 CPU capacity 没有超出当前 CPU 的限制.
    task p 可以在当前 CPU 上运行.

    • wake_affine 则为目标进程选择最合适运行的 wake CPU.

    ##2.1 want_affine

    有 wakeup 关系的进程都是相互关联的进程, 那么大概率 waker 和 wakee 之间有一些数据共享, 这些数据可能是 waker 进程刚刚准备好的, 还在 cache 里面, 那么把它唤醒到 waking CPU, 就能充分利用这些在 cache 中的数据. 但是另外一方面, waker 之前在 prev CPU 上运行, 那么也是 cache-hot 的, 把它迁移到 waking CPU 上, 那么 prev CPU 上那些 cache 就有可能失效, 因此如果 waker 和 wakee 之间没有数据共享或者共享的数据没那么多, 那么wake_affine 直接迁移到 waking CPU 上反而是不合适的.

    内核引入 wake_affine 的初衷就是识别什么时候要将 wakee 唤醒到 waking CPU, 什么时候不需要. 这个判断由 want_affine 通过 wake_cap() 和 wake_wide() 来完成.

    ###2.2.1 record_wakee 与 wakee_flips

    通过在 struct task_struct 中增加两个成员: 上次唤醒的进程 last_wakee, 和累积唤醒翻转计数器. 每当 waker 尝试唤醒 wakee 的时候, 就通过 record_wakee 来更新统计计数.

    在 select_task_rq_fair 开始的时候, 如果发现是 SD_BALANCE_WAKE, 则先会 record_wakee 统计 current 的 wakee_flips.

    static int
    select_task_rq_fair(struct task_struct *p, int prev_cpu, int sd_flag, int wake_flags)
    {
            if (sd_flag & SD_BALANCE_WAKE) {
                    record_wakee(p);
    

    wakee_flips 表示了当前进程作为 waker 时翻转(切换)其唤醒目标的次数, 所以高 wakee_flips 值意味着任务不止一个唤醒, 数字越大, 说明当前进程又不止一个 wakee, 而且唤醒频率越比较高. 且当前进程的 wakerr 数目 < wakee_flips.

    比如一个进程 P 连续一段时间的唤醒序列为: A, A, A, A, 那么由于没有翻转, 那么他的 wakee_flips 就始终为 1.

    static void record_wakee(struct task_struct *p)
    {
            /*
             * Only decay a single time; tasks that have less then 1 wakeup per
             * jiffy will not have built up many flips.
             */
            if (time_after(jiffies, current->wakee_flip_decay_ts + HZ)) {
                    current->wakee_flips >>= 1;
                    current->wakee_flip_decay_ts = jiffies;
            }
    
            if (current->last_wakee != p) {
                    current->last_wakee = p;
                    current->wakee_flips++;
            }
    }
    

    wakee_flips 有一定的衰减期, 如果过了 1S (即 1 个 HZ 的时间), 那么 wakee_flips 就衰减为原来的 1/2, 这就类似于 PELT 的指数衰减, Ns 前的 wakee_flips 的占比大概是当前这一个窗口的 1 / 2^N;

    全局变量jiffies用来记录自系统启动以来产生的节拍的总数(经过了多少tick). 启动时, 内核将该变量初始化为0, 此后, 每次时钟中断处理程序都会增加该变量的值.一秒内时钟中断的次数等于Hz, 所以jiffies一秒内增加的值也就是Hz.系统运行时间以秒为单位, 等于jiffies/Hz.
    将以秒为单位的时间转化为jiffies:
    seconds * Hz
    将jiffies转化为以秒为单位的时间:
    jiffies / Hz

    jiffies记录了系统启动以来, .

    一个tick代表多长时间, 在内核的CONFIG_HZ中定义.比如CONFIG_HZ=200, 则一个jiffies对应5ms时间.所以内核基于jiffies的定时器精度也是5ms

    ###2.2.2 wake_wide

    当前 current 正在为 wakeup p, 并为 p 选择一个合适的 CPU. 那么 wake_wide 就用来检查 current 和 p 之间是否适合 wake_affine 所关心的 waker/wakee 模型.

    wake_wide 返回 0, 表示 wake_affine 是有效的. 否则返回 1, 表示这两个进程不适合用 wake_affine.

    那么什么时候, wake_wide 返回 1 ?

    /*
     * Detect M:N waker/wakee relationships via a switching-frequency heuristic.
     *
     * A waker of many should wake a different task than the one last awakened
     * at a frequency roughly N times higher than one of its wakees.
     *
     * In order to determine whether we should let the load spread vs consolidating
     * to shared cache, we look for a minimum 'flip' frequency of llc_size in one
     * partner, and a factor of lls_size higher frequency in the other.
     *
     * With both conditions met, we can be relatively sure that the relationship is
     * non-monogamous, with partner count exceeding socket size.
     *
     * Waker/wakee being client/server, worker/dispatcher, interrupt source or
     * whatever is irrelevant, spread criteria is apparent partner count exceeds
     * socket size.
     */
    static int wake_wide(struct task_struct *p)
    {
            unsigned int master = current->wakee_flips;
            unsigned int slave = p->wakee_flips;
            int factor = this_cpu_read(sd_llc_size);
    
            if (master < slave)
                    swap(master, slave);
            if (slave < factor || master < slave * factor)
                    return 0;
            return 1;
    }
    

    wake_affine 在决策的时候, 要参考 wakee_flips

    1. 将 wakee_flips 值大的 wakee 唤醒到临近的 CPU, 可能有利于系统其他进程的唤醒, 同样这也意味着, waker 将面临残酷的竞争.
    2. 此外, 如果 waker 也有一个很高的 wakee_flips, 那意味着多个任务依赖它去唤醒, 然后 1 中造成的 waker 的更高延迟会对这些唤醒造成负面影响, 因此一个高 wakee_flips 的 waker 再去将另外一个高 wakee_flips 的 wakee 唤醒到本地的 CPU 上, 是非常不明智的决策. 因此, 当 waker-> wakee_flips / wakee-> wakee_flips 变得越来越高时, 进行 wake_affine 操作的成本会很高.

    理解了这层含义, 那我们 wake_wide 的算法就明晰了. 如下情况我们认为决策是有效的 wake_affine

    factor = this_cpu_read(sd_llc_size); 这个因子表示了在当前 NODE 上能够共享 cache 的 CPU 数目(或者说当前sched_domain 中 CPU 的数目), 一个 sched_domain 中, 共享 chache 的 CPU 越多(比如 X86 上一个物理 CPU 上包含多个逻辑 CPU), factor 就越大. 那么在 wake_affine 中的影响就是 wake_wide 返回 0 的概率更大, 那么 wake_affine 的结果有效的概率就更大. 因为有跟多的临近 CPU 可以选择, 这些 CPU 之间 cache 共享有优势.

    条件描述
    slave < factor即如果 wakee->wakee_flips < factor, 则说明当前进程的唤醒切换不那么频繁, 即使当前进程有 wakee_flips 个 wakee, 当前 sched_domain 也完全能装的下他们.
    master < slave * factor即 master/slave < factor, 两个 waker wakee_flips 的比值小于 factor, 那么这种情况下, 进行 wake_affine 的成本可控.
    commitpatchworklkml
    63b0e9edceec sched/fair: Beef up wake_widehttps://lore.kernel.org/patchwork/patch/576823https://lkml.org/lkml/2015/7/8/40

    ###2.2.3 wake_cap

    由于目前有一些 CPU 都是属于性能异构的 CPU(比如 ARM64 的 big.LITTLE 等), 不同的核 capacity 会差很多. wake_cap 会先看待选择的进程是否

    https://elixir.bootlin.com/linux/v5.6.13/source/kernel/sched/fair.c#L6128
    /*
     * Disable WAKE_AFFINE in the case where task @p doesn't fit in the
     * capacity of either the waking CPU @cpu or the previous CPU @prev_cpu.
     *
     * In that case WAKE_AFFINE doesn't make sense and we'll let
     * BALANCE_WAKE sort things out.
     */
    static int wake_cap(struct task_struct *p, int cpu, int prev_cpu)
    {
     long min_cap, max_cap;
    
     if (!static_branch_unlikely(&sched_asym_cpucapacity))
      return 0;
    
     min_cap = min(capacity_orig_of(prev_cpu), capacity_orig_of(cpu));
     max_cap = cpu_rq(cpu)->rd->max_cpu_capacity;
    
     /* Minimum capacity is close to max, no need to abort wake_affine */
     if (max_cap - min_cap < max_cap >> 3)
      return 0;
    
     /* Bring task utilization in sync with prev_cpu */
     sync_entity_load_avg(&p->se);
    
     return !task_fits_capacity(p, min_cap);
    }
    

    注意在 sched/fair: Capacity aware wakeup rework 合入之后, 通过 select_idle_sibling-=>elect_idle_capacity 让 wakeup 感知了 capacity, 因此 原生的 wakeup 路径无需再做 capacity 相关的处理, 因此 wake_cap 就被干掉了. 参见sched/fair: Remove wake_cap()

    ##2.3 wake_affine

    如果 want_affine 发现对当前 wakee 进行 wake_affine 是有意义的, 那么就会为当前进程选择一个能尽快运行的 CPU. 它总是倾向于选择 waking CPU(this_cpu) 以及 prev_cpu.

    其中

    • wake_affine_idle 则看 prev_cpu 以及 this_cpu 是不是处于 cache 亲和的以及是不是idle 状态, 这样的 CPU
      往往是最佳的.

    • wake_affine_weight 则进一步考虑进程的负载信息以及调度的延迟信息.

    /*
     * The purpose of wake_affine() is to quickly determine on which CPU we can run
     * soonest. For the purpose of speed we only consider the waking and previous
     * CPU.
     *
     * wake_affine_idle() - only considers 'now', it check if the waking CPU is
     * cache-affine and is (or will be) idle.
     *
     * wake_affine_weight() - considers the weight to reflect the average
     * scheduling latency of the CPUs. This seems to work
     * for the overloaded case.
     */
    static int wake_affine(struct sched_domain *sd, struct task_struct *p,
                           int this_cpu, int prev_cpu, int sync)
    {
            int target = nr_cpumask_bits;
    
            if (sched_feat(WA_IDLE))
                    target = wake_affine_idle(this_cpu, prev_cpu, sync);
    
            if (sched_feat(WA_WEIGHT) && target == nr_cpumask_bits)
                    target = wake_affine_weight(sd, p, this_cpu, prev_cpu, sync);
    
            schedstat_inc(p->se.statistics.nr_wakeups_affine_attempts);
            if (target == nr_cpumask_bits)
                    return prev_cpu;
    
            schedstat_inc(sd->ttwu_move_affine);
            schedstat_inc(p->se.statistics.nr_wakeups_affine);
            return target;
    }
    

    ###2.3.1 负载计算方式

    wake_affine 函数源码分析之前, 需要先知道三个load的计算方式如下:

    source_load(int cpu, int type)
    target_load(int cpu, int type)target_load(int cpu, int type)
    effective_load(struct task_group *tg, int cpu, long wl, long wg)
    

    根据调度类和 “nice” 值, 对迁移源 CPU 和目的 CPU 的负载 source_load 和 target_load 进行估计.
    对于 source_load 我们采用保守的方式进行估计, 对于 target_load 则倾向于偏激. 因此当 type 传入的值非 0 时, source_load 返回最小值, 而 target_load 返回最大值. 当 type == 0 时, 将直接返回 weighted_cpuload

    #https://elixir.bootlin.com/linux/v4.14.14/source/kernel/sched/fair.c#5258
    /*
     * Return a low guess at the load of a migration-source CPU weighted
     * according to the scheduling class and "nice" value.
     *
     * We want to under-estimate the load of migration sources, to
     * balance conservatively.
     */
    static unsigned long source_load(int cpu, int type)
    {
            struct rq *rq = cpu_rq(cpu);
            unsigned long total = weighted_cpuload(rq);
    
            if (type == 0 || !sched_feat(LB_BIAS))
                    return total;
    
            return min(rq->cpu_load[type-1], total);
    }
    
    #https://elixir.bootlin.com/linux/v4.14.14/source/kernel/sched/fair.c#5280
    /*
     * Return a high guess at the load of a migration-target CPU weighted
     * according to the scheduling class and "nice" value.
     */
    static unsigned long target_load(int cpu, int type)
    {
            struct rq *rq = cpu_rq(cpu);
            unsigned long total = weighted_cpuload(rq);
    
            if (type == 0 || !sched_feat(LB_BIAS))
                    return total;
    
            return max(rq->cpu_load[type-1], total);
    }
    
    #https://elixir.bootlin.com/linux/v4.14.14/source/kernel/sched/fair.c#5139
    /* Used instead of source_load when we know the type == 0 */
    static unsigned long weighted_cpuload(struct rq *rq)
    {
            return cfs_rq_runnable_load_avg(&rq->cfs);
    }
    

    ###2.3.2 wake_affine_idle

    static int
    wake_affine_idle(int this_cpu, int prev_cpu, int sync)
    {
            /*
             * If this_cpu is idle, it implies the wakeup is from interrupt
             * context. Only allow the move if cache is shared. Otherwise an
             * interrupt intensive workload could force all tasks onto one
             * node depending on the IO topology or IRQ affinity settings.
             *
             * If the prev_cpu is idle and cache affine then avoid a migration.
             * There is no guarantee that the cache hot data from an interrupt
             * is more important than cache hot data on the prev_cpu and from
             * a cpufreq perspective, it's better to have higher utilisation
             * on one CPU.
             */
            if (available_idle_cpu(this_cpu) && cpus_share_cache(this_cpu, prev_cpu))
                    return available_idle_cpu(prev_cpu) ? prev_cpu : this_cpu;
    
            if (sync && cpu_rq(this_cpu)->nr_running == 1)
                    return this_cpu;
    
            return nr_cpumask_bits;
    }
    

    如果 this_cpu 空闲, 则意味着唤醒来自中断上下文. 仅在 this_cpu 和 prev_cpu 有共享缓存时允许移动. 否则, 中断密集型工作负载可能会将所有任务强制到一个节点, 具体取决于IO拓扑或IRQ亲缘关系设置. 同时如果 prev_cpu 也是空闲的, 优先 prev_cpu.

    另外没有证据保证来自中断的缓存热数据比 prev_cpu 上的缓存热数据更重要, 并且从cpufreq的角度来看, 最好在一个CPU上获得更高的利用率.

    ###2.3.3 wake_affine_weight

    wake_affine_weight 会重新计算 wakeup CPUprev CPU 的负载情况, 如果 wakeup CPU 的负载加上唤醒进程的负载比 prev CPU 的负载小, 那么 wakeup CPU 是可以唤醒进程.

    static int
    wake_affine_weight(struct sched_domain *sd, struct task_struct *p,
                       int this_cpu, int prev_cpu, int sync)
    {
            s64 this_eff_load, prev_eff_load;
            unsigned long task_load;
    
            this_eff_load = target_load(this_cpu, sd->wake_idx);
    
            if (sync) {
                    unsigned long current_load = task_h_load(current);
    
                    if (current_load > this_eff_load)
                            return this_cpu;
    
                    this_eff_load -= current_load;
            }
    
            task_load = task_h_load(p);
    
            this_eff_load += task_load;
            if (sched_feat(WA_BIAS))
                    this_eff_load *= 100;
            this_eff_load *= capacity_of(prev_cpu);
    
            prev_eff_load = source_load(prev_cpu, sd->wake_idx);
            prev_eff_load -= task_load;
            if (sched_feat(WA_BIAS))
                    prev_eff_load *= 100 + (sd->imbalance_pct - 100) / 2;
            prev_eff_load *= capacity_of(this_cpu);
    
            /*
             * If sync, adjust the weight of prev_eff_load such that if
             * prev_eff == this_eff that select_idle_sibling() will consider
             * stacking the wakee on top of the waker if no other CPU is
             * idle.
             */
            if (sync)
                    prev_eff_load += 1;
    
            return this_eff_load < prev_eff_load ? this_cpu : nr_cpumask_bits;
    }
    

    我们假设将进程从 prev CPU 迁移到了 wakeup CPU, 那么 this_eff_load 记录了迁移后 wakeup CPU 的负载, 那么 prev_eff_load 则是迁移后 prev CPU 的负载.

    eff_load 的计算方式为:

    KaTeX parse error: Undefined control sequence: \task at position 18: …={[cpu\_load\pm\̲t̲a̲s̲k̲\_h\_load(p)]\t…

    注意使用 wake_affine_weight 需要开启 WA_WEIGHT 开关

    描述commit分析
    smart wake-affine(lkml,patchwork)

    wake_affine_weight 中负载比较的部分经历了很多次的修改.
    eeb603986391 sched/fair: Defer calculation of ‘prev_eff_load’ in wake_affine_weight() until needed
    082f764a2f3f sched/fair: Do not migrate on wake_affine_weight() if weights are equal
    1c1b8a7b03ef sched/fair: Replace source_load() & target_load() with weighted_cpuload(), 这个是 sched: remove cpu_loads 中的一个补丁, 该补丁集删除了 cpu_load idx 干掉了 LB_BIAS 特性, 它指出 LB_BIAS 的设计本身是有问题的, 在负载均衡迁移时平滑两个 cpu_load 的过程中, 用 source_load/target_load 的方式在源 CPU 和目的 CPU 上用一个随机偏差的方式是错误的, 这个平衡偏差应该取决于cpu组之间的任务转移成本,而不是随机历史记录或即时负载。因为历史负载可能与实际负载相差很大,从而导致不正确的偏差.
    11f10e5420f6c sched/fair: Use load instead of runnable load in wakeup path https://lore.kernel.org/patchwork/patch/1141693, 该补丁是 rework load balancce 的一个补丁, 之前唤醒路径用下的是 cpu_runnable_load, 现在修正为 cpu_load. cpu_load 对应的是 rq 的 load_avg, 代表就绪队列平均负载,其包含睡眠进程的负载贡献, cpu_runnable_load 则是 runnable_load_avg只包含就绪队列上所有可运行进程的负载贡献, wakeup 的时候如果使用 cpu_runnable_load 则可能造成选核的时候选择到一个有很多 runnable 线程的 overloaded 的 CPU, 而不是一个有很多 blocked 线程, 但是还有很大空闲的 CPU. 因此使用 cpu_load 在 wakeup 的时候可能更好.
    当前内核版本 5.6.13 中 wake_affine_weight 的实现参见, 跟我们前面将的思路没有太大变化, 但是没有了 LB_BIAS, 同时比较负载使用的是 cpu_load().

    ##2.4 wake_affine 演进

    Michael Wang 实现了 Smart wake affine, 引入 wakee_flips 来识别 wake-affine 的场景. 然后 Peter 做了一个简单的优化, factor 使用了 sd->sd_llc_size 而不是直接获取所在NODE 的 CPU 数目. nr_cpus_node(cpu_to_node(smp_processor_id()));

    commitlkmlpatchwork
    62470419e993 sched: Implement smarter wake-affine logic
    7d9ffa896148 sched: Micro-optimize the smart wake-affine logic
    https://lkml.org/lkml/2013/7/4/18https://lore.kernel.org/patchwork/cover/390846

    接着 Vincent Guittot 和 Rik van Riel 做了不少功耗优化的工作. 这时候 wake-affne 中开始考虑 CPU capacity 的信息.

    commitlkmlpatchwork
    05bfb65f52cb sched: Remove a wake_affine() condition
    bd61c98f9b3f sched: Test the CPU’s capacity in wake_affine()
    https://lkml.org/lkml/2014/5/23/458XXXX

    然后 Rik van Riel 在 NUMA 层次支持了 wake_affine

    commitlkmlpatchwork
    739294fb03f5 sched/numa: Override part of migrate_degrades_locality() when idle balancing
    7d894e6e34a5 sched/fair: Simplify wake_affine() for the single socket case
    3fed382b46ba sched/numa: Implement NUMA node level wake_affine()
    815abf5af45f sched/fair: Remove effective_load()
    https://lkml.org/lkml/2017/6/23/496https://lore.kernel.org/patchwork/cover/803114/

    紧接着是 Peter Zijlstra 的一堆 FIX, 为了解决支持了 NUMA 之后一系列性能问题.

    commitlkml描述
    90001d67be2f sched/fair: Fix wake_affine() for !NUMA_BALANCINGhttps://lkml.org/lkml/2017/8/1/377XXXX
    a731ebe6f17b sched/fair: Fix wake_affine_llc() balancing ruleshttps://lkml.org/lkml/2017/9/6/196XXXX
    d153b153446f sched/core: Fix wake_affine() performance regression
    f2cdd9cc6c97 sched/core: Address more wake_affine() regressions
    https://lkml.org/lkml/2017/10/14/172该补丁引入了 WA_IDLE, WA_BIAS+WA_WEIGHT

    目前最新 5.2 的内核中,
    Dietmar Eggemann 删除了 LB_BIAS 特性, 因此 wake-affine 的代码做了部分精简.(仅仅是代码重构, 没有逻辑变更)

    commitlkml描述
    fdf5f315d5cf sched/fair: Disable LB_BIAS by defaulthttps://lkml.org/lkml/2018/8/9/493默认 LB_BIAS 为 false
    1c1b8a7b03ef sched/fair: Replace source_load() & target_load() with weighted_cpuload()没有 LB_BIAS 之后, source_load/target_load 不再需要, 直接使用 weighted_cpuload 代替
    a3df067974c5 sched/fair: Rename weighted_cpuload() to cpu_runnable_load()weighted_cpuload 函数更名为 cpu_runnable_load, patchwork

    #3 wake_affine 对 select_task_rq_fair 的影响.

    在唤醒CFS 进程的时候通过 select_task_rq_fair 来为进程选择一个最适合的 CPU.

    try_to_wake_up
    cpu = select_task_rq(p, p->wake_cpu, SD_BALANCE_WAKE, wake_flags);
    

    那么在 wake_affine 机制的参与下, 选核流程是一个什么样的逻辑呢?
    代码参见, 选用早期 v5.1.15 版本的内核.

    • 首先 sd_flag 必须配置 SD_BALANCE_WAKE 才会去做 wake_affine, 如果是 energy aware, EAS 会先通过 find_energy_efficient_cpu 选核, 不过这个是 EAS 的范畴, 不是我们今天的重点.
    • 先 record_wakee 更新 wake affine 统计信息, 接着通过 wake_cap 和 wake_wide 看这次对进程的唤醒是不是 want_affine 的.
    • 接着从 waker CPU 开始向上遍历调度域,
    1.    如果是 want_affine, 则先通过 wake_affine 在当前调度域 tmp 中是从 prev_cpu 和 waker CPU 以及上次的 waker CPU( recent_used_cpu) 中优选一个合适的 new CPU, 待会选核的时候, 就会从走快速路径 select_idle_sibling 中从 prev_cpu 和 new cpu 中优选一个 CPU. 同时设置 recent_used_cpu 为当前 waker CPU
    2.    否则, 如果是 want_affine, 但是 tmp 中没找到满足要求的 CPU,  则最终循环结束条件为 !(tmp->flag & SD_LOAD_BALANCE), 同样如果不是 want_affine 的, 则最终循环结束条件为 !(tmp->flag & SD_LOAD_BALANCE)  或者 !tmp->flags & sd_flag,则 sd 指向了配置了 SD_LOAD_BALANCE 和 sd_flag 的最高那个层次的 sd, 这个时候会通过 find_idlest_cpu 慢速路径选择, 从这个 sd 中选择一个 idle 或者负载最小的 CPU. 
    

    只要 wakeup 的时候, 会通过 wake_affine, 然后通过 select_idle_sibling 来选核.
    其他情况下, 都是找到满足 sd_flags 的最高层次 sd, 然后通过 find_idlest_cpu 在这个调度域 sd 中去选择一个最空闲的 CPU.

    #4 参考资料

    Reduce scheduler migrations due to wake_affine

    [scheduler]十. 传统的负载均衡是如何为task选择合适的cpu?

    wukongmingjing 的调度器专栏

    Linux Kernel- task_h_load

    展开全文
  • Init进程是Linux 内核启动后创建的第一个用户进程,地位非常重要。 Init进程在初始化过程中会启动很多重要的守护进程,因此,了解Init进程的启动过程有助于我们更好的理解Android系统。 在介绍Init进程前,我们先...

    十一假期有点堕落,无限火力有点上瘾,谨戒、谨戒

    Init进程Linux 内核启动后创建的第一个用户进程,地位非常重要。

    Init进程在初始化过程中会启动很多重要的守护进程,因此,了解Init进程的启动过程有助于我们更好的理解Android系统。

    在介绍Init进程前,我们先简单介绍下Android的启动过程。从系统角度看,Android的启动过程可分为3个大的阶段:

    • bootloader引导
    • 装载和启动Linux内核
    • 启动Android系统,可分为
      • 启动Init进程
      • 启动Zygote
      • 启动SystemService
      • 启动SystemServer
      • 启动Home
      • 等等…

    我们看下启动过程图:
    image

    下面简单介绍下启动过程:

    1. Bootloader引导

    当按下电源键开机时,最先运行的就是Bootloader

    • Bootloader的主要作用是初始化基本的硬件设备(如 CPU、内存、Flash等)并且建立内存空间映射,为装载Linux内核准备好合适的运行环境。
    • 一旦Linux内核装载完毕,Bootloader将会从内存中清除掉
    • 如果在Bootloader运行期间,按下预定义的的组合键,可以进入系统的更新模块。Android的下载更新可以选择进入Fastboot模式或者Recovery模式:
      • FastbootAndroid设计的一套通过USB来更新Android分区映像的协议,方便开发人员快速更新指定分区。
      • RecoveryAndroid特有的升级系统。利用Recovery模式可以进行恢复出厂设置,或者执行OTA、补丁和固件升级。进入Recovery模式实际上是启动了一个文本模式的Linux
    1. 装载和启动Linux内核

    Android 的 boot.img 存放的就是Linux内核和一个根文件系统

    • Bootloader会把boot.img映像装载进内存
    • 然后Linux内核会执行整个系统的初始化
    • 然后装载根文件系统
    • 最后启动Init进程
    1. 启动Init进程

    Linux内核加载完毕后,会首先启动Init进程,Init进程是系统的第一个进程

    • Init进程启动过程中,会解析Linux的配置脚本init.rc文件。根据init.rc文件的内容,Init进程会:
      • 装载Android的文件系统
      • 创建系统目录
      • 初始化属性系统
      • 启动Android系统重要的守护进程,像USB守护进程adb守护进程vold守护进程rild守护进程
    • 最后,Init进程也会作为守护进程来执行修改属性请求,重启崩溃的进程等操作
    1. 启动ServiceManager

    ServiceManagerInit进程启动。在Binder 章节已经讲过,它的主要作用是管理Binder服务,负责Binder服务的注册与查找

    1. 启动Zygote进程

    Init进程初始化结束时,会启动Zygote进程。Zygote进程负责fork出应用进程,是所有应用进程的父进程

    • Zygote进程初始化时会创建Android 虚拟机、预装载系统的资源文件和Java
    • 所有从Zygote进程fork出的用户进程都将继承和共享这些预加载的资源,不用浪费时间重新加载,加快的应用程序的启动过程
    • 启动结束后,Zygote进程也将变为守护进程,负责响应启动APK的请求
    1. 启动SystemServer

    SystemServerZygote进程fork出的第一个进程,也是整个Android系统的核心进程

    • SystemServer中运行着Android系统大部分的Binder服务
    • SystemServer首先启动本地服务SensorManager
    • 接着启动包括ActivityManagerServiceWindowsManagerServicePackageManagerService在内的所有Java服务
    1. 启动MediaServer

    MediaServerInit进程启动。它包含了一些多媒体相关的本地Binder服务,包括:CameraServiceAudioFlingerServiceMediaPlayerServiceAudioPolicyService

    1. 启动Launcher
    • SystemServer加载完所有的Java服务后,最后会调用ActivityManagerServiceSystemReady()方法
    • SystemReady()方法中,会发出Intent<android.intent,category.HOME>
    • 凡是响应这个Intentapk都会运行起来,一般Launcher应用才回去响应这个Intent

    Init进程的初始化过程

    Init进程的源码目录在system/core/init下。程序的入口函数main()位于文件init.c

    main()函数的流程

    书中使用的是Android 5.0源码,相比Android 9.0这部分已经有很多改动,不过大的方向是一致的,只能对比着学习了。。。

    main()函数比较长,整个Init进程的启动流程都在这个函数中。由于涉及的点比较多,这里我们先了解整体流程,细节后面补充,一点一点来哈

    Init进程的main()函数的结构是这样的:

    int main(int argc, char** argv) {
        //启动参数判断部分
        
        if (is_first_stage) {
            //初始化第一阶段部分
        }
        
        //初始化第二阶段部分
    
        while (true) {
            //一个无限循环部分
        }
    

    启动程序参数判断

    进入main()函数后,首先检查启动程序的文件名

    函数源码:

        if (!strcmp(basename(argv[0]), "ueventd")) {
            return ueventd_main(argc, argv);
        }
        if (!strcmp(basename(argv[0]), "watchdogd")) {
            return watchdogd_main(argc, argv);
        }
    
    • 如果文件名是ueventd,执行ueventd守护进程的主函数ueventd_main
    • 如果文件名是watchdogd,执行watchdogd守护进程的主函数watchdogd_main
    • 都不是,则继续执行

    才开始是不是就已经有些奇怪了,Init进程中还包含了另外两个守护进程的启动,这主要是因为这几个守护进程的代码重合度高,开发人员干脆都放在一起了。

    我们看一下Android.mk中的片段:

    # Create symlinks.
    LOCAL_POST_INSTALL_CMD := $(hide) mkdir -p $(TARGET_ROOT_OUT)/sbin; \
        ln -sf ../init $(TARGET_ROOT_OUT)/sbin/ueventd; \
        ln -sf ../init $(TARGET_ROOT_OUT)/sbin/watchdogd
    
    • 在编译时,Android生成了两个指向init文件的符号链接ueventdwatchdogd
    • 这样,启动时如果执行的是这两个符号链接,main()函数就可以根据名称判断到底启动哪一个

    初始化的第一阶段

    设置文件属性掩码

    函数源码:

        // Clear the umask.
        umask(0);
    

    默认情况下一个进程创建出的文件合文件夹的属性是022,使用umask(0)意味着进程创建的属性是0777

    mount相应的文件系统

    函数源码:

            // Get the basic filesystem setup we need put together in the initramdisk
            // on / and then we'll let the rc file figure out the rest.
            mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
            mkdir("/dev/pts", 0755);
            mkdir("/dev/socket", 0755);
            mount("devpts", "/dev/pts", "devpts", 0, NULL);
            #define MAKE_STR(x) __STRING(x)
            mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
            //......
            mount("sysfs", "/sys", "sysfs", 0, NULL);
            mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);
            //......
            // Mount staging areas for devices managed by vold
            // See storage config details at http://source.android.com/devices/storage/
            mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                  "mode=0755,uid=0,gid=1000");
            // /mnt/vendor is used to mount vendor-specific partitions that can not be
            // part of the vendor partition, e.g. because they are mounted read-write.
            mkdir("/mnt/vendor", 0755);
            InitKernelLogging(argv);
    

    创建一些基本的目录,包括/dev/proc/sys等,同时把一些文件系统,如tmpfsdevptprocsysfsmount到相应的目录

    • tmpfs是一种基于内存的文件系统,mount后就可以使用。
      • tmpfs文件系统下的文件都放在内存中,访问速度快,但是掉电丢失。因此适合存放一些临时性的文件
      • tmpfs文件系统的大小是动态变化的,刚开始占用空间很小,随着文件的增多会随之变大
      • Androidtmpfs文件系统mount/dev目录。/dev目录用来存放系统创造的设备节点
    • devpts是虚拟终端文件系统,通常mount/dev/pts目录下
    • proc也是一种基于内存的虚拟文件系统,它可以看作是内核内部数据结构的接口
      • 通过它可以获得系统的信息
      • 同时能够在运行时修改特定的内核参数
    • sysfs文件系统和proc文件系统类似,它是在Linux 2.6内核引入的,作用是把系统设备和总线按层次组织起来,使得他们可以在用户空间存取

    初始化kernelLog系统

    通过InitKernelLogging()函数进行初始化,由于此时Androidlog系统还没有启动,所以Init只能使用kernellog系统

            // Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
            // talk to the outside world...
            InitKernelLogging(argv);
    

    初始化SELinux

            // Set up SELinux, loading the SELinux policy.
            SelinuxSetupKernelLogging();
            SelinuxInitialize();
    

    SELinux是在Android 4.3加入的安全内核,后面详细介绍

    初始化的第二阶段

    创建.booting空文件

    /dev目录下创建一个空文件.booting表示初始化正在进行

        // At this point we're in the second stage of init.
        InitKernelLogging(argv);
        LOG(INFO) << "init second stage started!";
        //......
        // Indicate that booting is in progress to background fw loaders, etc.
        close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
    

    大家留心注释,我们已经处于初始化的第二阶段了

    • is_booting()函数会依靠空文件.booting来判断是否进程处于初始化中
    • 初始化结束后这个文件将被删除

    初始化Android的属性系统

        property_init();
    

    property_init()函数主要作用是创建一个共享区域来储存属性值,后面会详细介绍

    解析kernel参数并进行相关设置

        // If arguments are passed both on the command line and in DT,
        // properties set in DT always have priority over the command-line ones.
        process_kernel_dt();
        process_kernel_cmdline();
    
        // Propagate the kernel variables to internal variables
        // used by init as well as the current required properties.
        export_kernel_boot_props();
    
        // Make the time that init started available for bootstat to log.
        property_set("ro.boottime.init", getenv("INIT_STARTED_AT"));
        property_set("ro.boottime.init.selinux", getenv("INIT_SELINUX_TOOK"));
    
        // Set libavb version for Framework-only OTA match in Treble build.
        const char* avb_version = getenv("INIT_AVB_VERSION");
        if (avb_version) property_set("ro.boot.avb_version", avb_version);
    

    这部分进行的是属性的设置,我们看下几个重点方法:

    • process_kernel_dt()函数:读取设备树(DT)上的属性设置信息,查找系统属性,然后通过property_set设置系统属性
    • process_kernel_cmdline()函数:解析kernelcmdline文件提取以 androidboot.字符串打头的字符串,通过property_set设置该系统属性
    • export_kernel_boot_props()函数:额外设置一些属性,这个函数中定义了一个集合,集合中定义的属性都会从kernel中读取并记录下来

    进行第二阶段的SELinux设置

    进行第二阶段的SELinux设置并恢复一些文件安全上下文

        // Now set up SELinux for second stage.
        SelinuxSetupKernelLogging();
        SelabelInitialize();
        SelinuxRestoreContext();
    

    初始化子进程终止信号处理函数

        epoll_fd = epoll_create1(EPOLL_CLOEXEC);
        if (epoll_fd == -1) {
            PLOG(FATAL) << "epoll_create1 failed";
        }
    
        sigchld_handler_init();
    

    linux当中,父进程是通过捕捉SIGCHLD信号来得知子进程运行结束的情况,SIGCHLD信号会在子进程终止的时候发出。

    • 为了防止init的子进程成为僵尸进程(zombie process),init 在子进程结束时获取子进程的结束码
    • 通过结束码将程序表中的子进程移除,防止成为僵尸进程的子进程占用程序表的空间
    • 程序表的空间达到上限时,系统就不能再启动新的进程了,这样会引起严重的系统问题

    设置系统属性并开启属性服务

        property_load_boot_defaults();
        export_oem_lock_status();
        start_property_service();
        set_usb_controller();
    
    • property_load_boot_defaults()export_oem_lock_status()set_usb_controller()这三个函数都是调用、设置一些系统属性
    • start_property_service():开启系统属性服务

    加载init.rc文件

    init.rc是一个可配置的初始化文件,在Android中被用作程序的启动脚本,它是run commands运行命令的缩写

    通常第三方定制厂商可以配置额外的初始化配置:init.%PRODUCT%.rc。在init的初始化过程中会解析该配置文件,完成定制化的配置过程。

    init.rc文件的规则和具体解析逻辑后面详解,先看下它在main函数中的相关流程。

    函数代码:

        const BuiltinFunctionMap function_map;
        Action::set_function_map(&function_map);
    
        subcontexts = InitializeSubcontexts();
        // 创建 action 相关对象
        ActionManager& am = ActionManager::GetInstance();
        // 创建 service 相关对象
        ServiceList& sm = ServiceList::GetInstance();
        // 加载并解析init.rc文件到对应的对象中
        LoadBootScripts(am, sm);
    

    解析完成后,会执行

        am.QueueEventTrigger("early-init");
    
        // Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...
        // 等待冷插拔设备初始化完成
        am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
        // ... so that we can start queuing up actions that require stuff from /dev.
        am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
        am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
        am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");
        // 初始化组合键监听模块
        am.QueueBuiltinAction(keychord_init_action, "keychord_init");
        // 在屏幕上显示 Android 字样的Logo
        am.QueueBuiltinAction(console_init_action, "console_init");
    
        // Trigger all the boot actions to get us started.
        am.QueueEventTrigger("init");
    
        // Starting the BoringSSL self test, for NIAP certification compliance.
        am.QueueBuiltinAction(StartBoringSslSelfTest, "StartBoringSslSelfTest");
    
        // Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
        // wasn't ready immediately after wait_for_coldboot_done
        am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
    
        // Don't mount filesystems or start core system services in charger mode.
        std::string bootmode = GetProperty("ro.bootmode", "");
        if (bootmode == "charger") {
            am.QueueEventTrigger("charger");
        } else {
            am.QueueEventTrigger("late-init");
        }
    
        // Run all property triggers based on current state of the properties.
        am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");
    
    • am.QueueEventTrigger函数即表明到达了某个所需的某个时间条件
      • am.QueueEventTrigger("early-init")表明early-init条件触发,对应的动作可以开始执行
      • 需要注意的是这个函数只是将时间点(如:early-init)填充进event_queue_运行队列
      • 后面的while(true)循环才会真正的去按顺序取出,并触发相应的操作

    到这里,

    • init.rc相关的actionservice已经解析完成
    • 对应的列表也已经准备就绪
    • 对应的Trigger也已经添加完成

    接下来就是执行阶段了:

        while (true) {
            // 执行命令列表中的 Action
            if (!(waiting_for_prop || Service::is_exec_service_running())) {
                am.ExecuteOneCommand();
            }
            if (!(waiting_for_prop || Service::is_exec_service_running())) {
                if (!shutting_down) {
                    // 启动服务列表中的服务
                    auto next_process_restart_time = RestartProcesses();
                    //......
                }
                //......
            }
            
            // 监听子进程的死亡通知信号
            epoll_event ev;
            int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, epoll_timeout_ms));
            if (nr == -1) {
                PLOG(ERROR) << "epoll_wait failed";
            } else if (nr == 1) {
                ((void (*)()) ev.data.ptr)();
            }
        }
    

    到这里,main函数的整体流程就分析完了。分析过程中我们舍弃了很多细节,接下来就是填补细节的时候了。

    启动Service进程

    main函数的while()循环中会调用RestartProcesses()来启动服务列表中的服务进程,我们看下函数源码:

    static std::optional<boot_clock::time_point> RestartProcesses() {
        //......
        //循环检查每个服务
        for (const auto& s : ServiceList::GetInstance()) {
            // 判断标志位是否为 SVC_RESTARTING
            if (!(s->flags() & SVC_RESTARTING)) continue;
            // ......省略时间相关的判断
            // 启动服务进程
            auto result = s->Start();
        }
        //......
    }
    

    RestartProcesses()会检查每个服务:凡是带有SVC_RESTARTING标志的,才会执行服务的启动s->Start();

    其实重点在s->Start();方法,我们具体来看下(删减版):

    Result<Success> Service::Start() {
    
        //......省略部分
        // 清空service相关标记
        // 判断service状态,如果已运行直接返回
        // 判断service二进制文件是否存在
        // 初始化console、scon(安全上下文)等
        //......省略部分
    
        pid_t pid = -1;
        if (namespace_flags_) {// 当service定义了namespace时会赋值为CLONE_NEWPID|CLONE_NEWNS
            pid = clone(nullptr, nullptr, namespace_flags_ | SIGCHLD, nullptr);
        } else {
            pid = fork();
        }
    
        if (pid == 0) {// 子进程创建成功
            //......省略部分
            // setenv、writepid、重定向标准IO
            //......省略部分
    
            // As requested, set our gid, supplemental gids, uid, context, and
            // priority. Aborts on failure.
            SetProcessAttributes();
    
            if (!ExpandArgsAndExecv(args_)) {// 解析参数并启动service
                PLOG(ERROR) << "cannot execve('" << args_[0] << "')";
            }
            // 不太懂这里退出的目的是干啥
            _exit(127);
        }
    
        if (pid < 0) {
            // 子进程创建失败
            pid_ = 0;
            return ErrnoError() << "Failed to fork";
        }
    
        //......省略部分
        // 执行service其他参数的设置,如oom_score_adj、创建并设置ProcessGroup相关的参数
        //......省略部分
        NotifyStateChange("running");
        return Success();
    }
    

    Service进程的启动流程还有很多细节,这部分只是简单介绍下流程,涉及的sconPIDSIDPGID等东西还很多。

    偷懒啦!能力、时间有限,先往下学习,待用到时再来啃吧

    解析启动脚本init.rc

    Init进程启动时最重要的工作就是解析并执行启动文件init.rc官方说明文档下载链接

    init.rc文件格式

    init.rc文件是以section为单位组织的

    • section分为两大类:

      • action:以关键字on开始,表示一堆命令的集合
      • service:以关键字service开始,表示启动某个进程的方式和参数
    • section以关键字onservice开始,直到下一个onservice结束

    • section中的注释以#开始

    打个样儿:

    import /init.usb.rc
    import /init.${ro.hardware}.rc
    
    on early-init
        mkdir /dev/memcg/system 0550 system system
        start ueventd
    
    on init
        symlink /system/bin /bin
        symlink /system/etc /etc
    
    on nonencrypted
        class_start main
        class_start late_start
        
    on property:sys.boot_from_charger_mode=1
        class_stop charger
        trigger late-init
    
    service ueventd /sbin/ueventd
        class core
        critical
        seclabel u:r:ueventd:s0
        shutdown critical
    
    service flash_recovery /system/bin/install-recovery.sh
        class main
        oneshot
    
    service ueventd /sbin/ueventd
        class core
        critical
        seclabel u:r:ueventd:s0
        shutdown critical
    

    无论是action还是service,并不是按照文件中的书写顺序执行的,执行与否以及何时执行要由Init进程在运行时决定。

    对于init.rcaction

    • 关键字on后面跟的字符串称为trigger,如上面的early-initinit等。
    • trigger后面是命令列表,命令列表中的每一行就是一条命令。

    对于init.rcservice

    • 关键字service后面是服务名称,可以使用start服务名称来启动一个服务。如start ueventd
    • 服务名称后面是进程的可执行文件的路径和启动参数
    • service下面的行称为option,每个option占一行
      • 例如:class main中的class表示服务所属的类别,可以通过class_start来启动一组服务,像class_start main

    想要了解更多,可以参考源码中的README文档,路径是system/core/init/README.md

    init.rc的关键字

    这部分是对system/core/init/README.md文档的整理,挑重点记录哈

    Androidrc脚本包含了4中类型的声明:ActionCommandsServicesOptions

    • 所有的指令都以行为单位,各种符号则由空格隔开。
    • c语言风格的反斜杠\可用于在符号间插入空格
    • 双引号""可用于防止字符串被空格分割成多个记号
    • 行末的反斜杠\可用于折行
    • 注释以#开头
    • ActionServices用来申明一个分组
      • 所有的CommandsOptions都属于最近声明的分组
      • 位于第一个分组之前的CommandsOptions将被忽略

    Actions

    Actions是一组Commands的集合。每个Action都有一个trigger用来决定何时执行。当触发条件与Actiontrigger匹配一致时,此Action会被加入到执行队列的尾部

    每个Action都会依次从队列中取出,此Action的每个Command都将依次执行。

    Actions格式如下:

    on  < trigger >
        < command >
        < command >
        < command >
    

    Services

    通过Services定义的程序,会在Init中启动,如果退出了会被重启。

    Services的格式如下:

    service <name> <pathname> [ <argument> ]*
       <option>
       <option>
       ...
    

    Options

    OptionsServices的修订项。它们决定了一个Service何时以及如何运行。

    • critical:表示这是一个关键的Service,如果Service4分钟内重新启动超过4次,系统将自动重启并进入recovery模式
    • console [<console>]:表示该服务需要一个控制台
      • 第二个参数用来指定特定控制台名称,默认为/dev/console
      • 指定时需省略掉/dev/部分,如/dev/tty0需写成console tty0
    • disabled:表示服务不会通过class_start启动。它必须以命令start service_name的方式指定来启动
    • setenv <name> <value>:在Service启动时将环境变量name设置为value
    • socket <name> <type> <perm> [ <user> [ <group> [ <seclabel> ] ] ]:创建一个名为/dev/socket/<name>的套接字,并把文件描述符传递给要启动的进程
      • type的值必须是dgramstreamseqpacket
      • usergroup默认为0
      • seclabel是这个socketSElinux安全上下文,默认为当前service的上下文
    • user <username>:在执行此服务之前切换用户名,默认的是root。如果进程没有相应的权限,则不能使用该命令
    • oneshotService退出后不再重启
    • class <name>:给Service指定一个名字。所有同名字的服务可以同时启动和停止。如果不通过class显示指定,默认为default
    • onrestart:当Service重启时,执行一条命令

    还有很多哈,就不一一介绍了,像shutdown <shutdown_behavior>这种,参照官方说明就好啦

    Triggers

    trigger本质上是一个字符串,能够匹配某种包含该字符串的事件。trigger又被细分为事件触发器(event trigger)属性触发器(property trigger)

    • 事件触发器可由trigger命令或初始化过程中通过QueueEventTrigger()触发

      • 通常是一些事先定义的简单字符串,例如:boot,late-init
    • 属性触发器是当指定属性的变量值变成指定值时触发

      • 其格式为property:<name>=*

    请注意,一个Action可以有多个属性触发器,但是最多有一个事件触发器。看下官方的例子:

    on boot && property:a=b
    

    上面的Action只有在boot事件发生时,并且属性a数值b相等的情况下才会被触发。

    而对于下面的Action

    on property:a=b && property:c=d 
    

    存在三种触发情况:

    • 在启动时,如果属性a的值等于b并且属性c的值等于d
    • 属性c的值已经是d的情况下,属性a的值被更新为b
    • 属性a的值已经是b的情况下,属性c的值被更新为d

    对于事件触发器,大体包括:

    类型说明
    bootinit.rc被装载后触发
    device-added-<path>指定设备被添加时触发
    device-removed-<path>指定设备被移除时触发
    service-exited-<name>在特定服务退出时触发
    early-init初始化之前触发
    late-init初始化之后触发
    init初始化时触发

    Commands

    Command是用于Action的命令列表或者ServiceOption<onrestart>中,在源码中是这样的:

    static const Map builtin_functions = {
            {"chmod",                   {2,     2,    {true,   do_chmod}}},
            {"chown",                   {2,     3,    {true,   do_chown}}},
            {"class_start",             {1,     1,    {false,  do_class_start}}},
            {"class_stop",              {1,     1,    {false,  do_class_stop}}},
            ......
        };
    

    看几个常用的吧

    1. bootchart [start|stop]:开启或关闭进程启动时间记录工具
        //init.rc file
        mkdir /data/bootchart 0755 shell shell
        bootchart start
    
    • Init进程中会启动bootchart,默认不会执行时间采集
    • 当我们需要采集启动时间时,需创建一个/data/bootchart/enabled文件
    1. chmod <octal-mode> <path>:更改文件权限
    chmode 0755 /metadata/keystone
    
    1. chown <owner> <group> <path>:更改文件的所有者和组
    chown system system /metadata/keystone
    
    1. mkdir <path> [mode] [owner] [group]:创建指定目录
    mkdir /data/bootchart 0755 shell shell
    
    1. trigger <event>:触发某个事件(Action),用于将该事件排在某个事件之后
    on late-init
        trigger early-fs
        trigger fs
        trigger post-fs
        trigger late-fs
        trigger post-fs-data
        trigger zygote-start
        trigger load_persist_props_action
        trigger early-boot
        trigger boot
    
    1. class_start <serviceclass>:启动所有指定服务class下的未运行服务
        class_start main
        class_start late_start
    
    1. class_stop <serviceclass>:停止所有指定服务class下的已运行服务
        class_stop charger
    
    1. exec [ <seclabel> [ <user> [ <group>\* ] ] ] -- <command> [ <argument>\* ]:通过给定的参数fork和启动一个命令。
    • 具体的命令在--后开始
    • 参数包括seclable(默认的话使用-)、usergroup
    • exec为阻塞式,在当前命令完成前,不会运行其它命令,此时Init进程暂停执行。
    exec - system system -- /system/bin/tzdatacheck /system/usr/share/zoneinfo /data/misc/zoneinfo
    

    还有很多指令就不一一介绍了,参考官方文档和源码就好啦

    init脚本的解析

    上面我们知道了,在init.cppmain函数中通过LoadBootScripts()来加载rc脚本,我们简单看下解析流程(注释比较详细啦)

    /**
     * 7.0后,init.rc进行了拆分,每个服务都有自己的rc文件
     * 他们基本上都被加载到/system/etc/init,/vendor/etc/init, /odm/etc/init等目录
     * 等init.rc解析完成后,会来解析这些目录中的rc文件,用来执行相关的动作
     * ===============================================================================
     * 对于自定义的服务来说,我们只需要通过 LOCAL_INIT_RC 指定自己的rc文件即可
     * 编译时会根据分区标签将rc文件拷贝到指定的partition/etc/init目录
     */
    static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
        // 创建解析器
        Parser parser = CreateParser(action_manager, service_list);
        std::string bootscript = GetProperty("ro.boot.init_rc", "");
        if (bootscript.empty()) {
            // 如果没有特殊配置ro.boot.init_rc,先解析 init.rc 文件
            parser.ParseConfig("/init.rc");
            if (!parser.ParseConfig("/system/etc/init")) {
                late_import_paths.emplace_back("/system/etc/init");
            }
            if (!parser.ParseConfig("/product/etc/init")) {
                late_import_paths.emplace_back("/product/etc/init");
            }
            if (!parser.ParseConfig("/odm/etc/init")) {
                late_import_paths.emplace_back("/odm/etc/init");
            }
            if (!parser.ParseConfig("/vendor/etc/init")) {
                late_import_paths.emplace_back("/vendor/etc/init");
            }
        } else {
            // 直接解析 ro.boot.init_rc 中的数据
            parser.ParseConfig(bootscript);
        }
    }
    
    /**
     * 创建解析器,目前只有三种section:service、on、import
     * 与之对应的,函数中也出现了三种解析器:ServiceParser、ActionParser、ImportParser
     */
    Parser CreateParser(ActionManager& action_manager, ServiceList& service_list) {
        Parser parser;
        parser.AddSectionParser("service", std::make_unique<ServiceParser>(&service_list, subcontexts));
        parser.AddSectionParser("on", std::make_unique<ActionParser>(&action_manager, subcontexts));
        parser.AddSectionParser("import", std::make_unique<ImportParser>(&parser));
        return parser;
    }
    

    init中启动的守护进程

    init.rc中定义了很多守护进程,9我们来看下相关内容:

    # adb 守护进程放在了这里
    import /init.usb.rc
    # service adbd /system/bin/adbd --root_seclabel=u:r:su:s0
    #    class core
    #    ......
    
    on boot
        # Start standard binderized HAL daemons
        class_start hal
        # 启动所有class core的服务
        # 像adb、console等
        class_start core
    
    on eraly-init
        start ueventd
    
    on post-fs
        # Load properties from
        #     /system/build.prop,
        #     /odm/build.prop,
        #     /vendor/build.prop and
        #     /factory/factory.prop
        load_system_props
        # start essential services
        # 这几个 service 的.rc文件都在对应项目中,通过LOCAL_INIT_RC来指定
        start logd
        # 启动三个Binder服务管理相关的service
        # servicemanager用于框架/应用进程之间的 IPC,使用 AIDL 接口
        start servicemanager
        # hwservicemanager用于框架/供应商进程之间的 IPC,使用 HIDL 接口
        # 也可用于供应商进程之间的 IPC,使用 HIDL 接口
        start hwservicemanager
        # vndservicemanager用于供应商/供应商进程之间的 IPC,使用 AIDL 接口
        start vndservicemanager
    
    on post-fs-data
        # 启动 vold(Volume守护进程),负责系统扩展储存的自动挂载
        # 这个进程后面详解
        start vold
    
    # 负责响应 uevent 事件,创建对应的设备节点
    service ueventd /sbin/ueventd
        class core
        ......
        
    # 包含常用的shell命令,如ls、cd等
    service console /system/bin/sh
        class core
        ......
    
    # Zygote 进程在这里导入,现在支持32位和64位
    import /init.${ro.zygote}.rc
    # zygote中会启动一些相关service,像media、netd、wificond等
    # service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
    #    onrestart restart audioserver
    #    onrestart restart cameraserver
    #    onrestart restart media
    #    onrestart restart netd
    #    onrestart restart wificond
    # 启动 Zygote 进程,触发这个action的位置在 on late-init 中
    on zygote-start && property:ro.crypto.state=unencrypted
        start netd
        start zygote
        start zygote_secondary
    

    启动流程和需要启动的service通过init.rc基本上就可以完成定制。

    感觉Init进程通过解析*.rc的方式大大简化了开发,真的是6啊。设计这一套AIL的人是真滴猛。。。。。。。

    Init进程对信号的处理

    Init进程是系统的一号进程,系统中的其他进程都是Init进程的后代.

    按照Linux的设计,Init进程需要在这些后代死亡时负责清理它们,以防止它们变成僵尸进程

    僵尸进程简介

    关于僵尸进程可以参考Wiki百科-僵尸进程

    类UNIX系统中,僵尸进程是指完成执行(通过exit系统调用,或运行时发生致命错误或收到终止信号所致),但在操作系统的进程表中仍然存在其进程控制块,处于终止状态的进程。

    这发生于子进程需要保留表项以允许其父进程读取子进程的退出状态:一旦退出态通过wait系统调用读取,僵尸进程条目就从进程表中删除,称之为回收(reaped)

    正常情况下,进程直接被其父进程wait并由系统回收。

    僵尸进程的避免

    • 父进程通过waitwaitpid等函数等待子进程结束,这会导致父进程挂起
    • 如果父进程很忙,那么可以用signal函数为SIGCHLD安装handler,因为子进程结束后, 父进程会收到该信号,可以在handler中调用wait回收
    • 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD,SIG_IGN) 通知内核,自己对子进程的结束不感兴趣,那么子进程结束后,内核会回收,并不再给父进程发送信号
    • 还有一些技巧,就是fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程init接管,孙进程结束后,Init会回收。不过子进程的回收 还要自己做

    我们下面来看下Init进程怎么处理的

    初始化SIGCHLD信号

    Init进程的main函数中,在初始化第二阶段,有这么一个方法sigchld_handler_init()

    // file : init.cpp
    int main(int argc, char** argv) {
        ......
        sigchld_handler_init();
        ......
    }
    // file : sigchld_handler.cpp
    void sigchld_handler_init() {
        // Create a signalling mechanism for SIGCHLD.
        // 创建一个socketpair,往一个socket中写,就可以从另外一个套接字中读取到数据
        int s[2];
        socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s);
        signal_write_fd = s[0];
        signal_read_fd = s[1];
    
        // Write to signal_write_fd if we catch SIGCHLD.
        // 信号初始化相关参数设置
        // 设置SIGCHLD_handler信号处理函数
        // 设置SA_NOCLDSTOP标志
        struct sigaction act;
        memset(&act, 0, sizeof(act));
        act.sa_handler = SIGCHLD_handler;
        act.sa_flags = SA_NOCLDSTOP;
        
        // 注册SIGCHLD信号
        sigaction(SIGCHLD, &act, 0);
    
        ReapAnyOutstandingChildren();
        
        // 注册signal_read_fd到epoll_fd
        register_epoll_handler(signal_read_fd, handle_signal);
    }
    // file : sigchld_handler.cpp
    /**
     * SIGCHLD_handler的作用是当init进程接收到SIGCHLD信号时,往signal_write_fd中写入数据
     * 这个时候套接字对中的另外一个signal_read_fd就可读了。
     */
    static void SIGCHLD_handler(int) {
        if (TEMP_FAILURE_RETRY(write(signal_write_fd, "1", 1)) == -1) {
            PLOG(ERROR) << "write(signal_write_fd) failed";
        }
    }
    // file : sigchld_handler.cpp
    /**
     * register_epoll_handler函数主要的作用是注册属性socket文件描述符到轮询描述符epoll_fd
     */
    void register_epoll_handler(int fd, void (*fn)()) {
        epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.ptr = reinterpret_cast<void*>(fn);
        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
            PLOG(ERROR) << "epoll_ctl failed";
        }
    }
    

    信号的初始化是通过系统调用sigaction()来完成的

    • 参数act中:
      • sa_handler用来指定信号的处理函数
      • sa_flags用来指定触发标志,SA_NOCLDSTOP标志意味着当子进程终止时才接收SIGCHLD信号

    Linux系统中,信号又称为软中断,信号的到来会中断进程正在处理的工作,因此在信号处理函数中不要去调用一些不可重入的函数。而且Linux不会对信号排队,不管是在信号的处理期间再来多少个信号,当前的信号处理函数执行完后,内核只会再发送一个信号给进程,因此,为了不丢失信号,我们的信号处理函数执行得越快越好

    而对于SIGCHLD信号,父进程需要执行等待操作,这样的话时间就比较长了,因此需要有办法解决这个矛盾

    • 上面的代码中创建了一对本地socket用于进程间通信
    • 当信号到来时,SIGCHLD_handler处理函数只要向socket中的signal_write_fd写入数据就可以
    • 这样,信号的处理就转变到了socket的处理上了

    此时,我们需要监听signal_read_fd,并提供一个回调函数,这就是register_epoll_handler()函数的作用

    • 函数中的EPOLLIN表示当文件描述符可读时才会触发
    • *fn就是触发后的回调函数指针,赋值给了ev.data.ptr,请留意下这个指针变量,后面会用到
    • 提供的回调函数就是handle_signal()

    响应子进程的死亡事件

    Init进程启动完毕后,会监听创建的socket,如果有数据到来,主线程会唤醒并调用处理函数handle_signal()

    static void handle_signal() {
        // Clear outstanding requests.
        // 清空 signal_read_fd 中的数据
        char buf[32];
        read(signal_read_fd, buf, sizeof(buf));
        
        ReapAnyOutstandingChildren(); 
    }
    void ReapAnyOutstandingChildren() {
        while (ReapOneProcess()) {
        }
    }
    static bool ReapOneProcess() {
        siginfo_t siginfo = {};
        // This returns a zombie pid or informs us that there are no zombies left to be reaped.
        // It does NOT reap the pid; that is done below.
        // 查询是否存在僵尸进程
        if (TEMP_FAILURE_RETRY(waitid(P_ALL, 0, &siginfo, WEXITED | WNOHANG | WNOWAIT)) != 0) {
            PLOG(ERROR) << "waitid failed";
            return false;
        }
        // 没有僵尸进程直接返回
        auto pid = siginfo.si_pid;
        if (pid == 0) return false;
    
        // At this point we know we have a zombie pid, so we use this scopeguard to reap the pid
        // whenever the function returns from this point forward.
        // We do NOT want to reap the zombie earlier as in Service::Reap(), we kill(-pid, ...) and we
        // want the pid to remain valid throughout that (and potentially future) usages.
        // 等待子进程终止
        auto reaper = make_scope_guard([pid] { TEMP_FAILURE_RETRY(waitpid(pid, nullptr, WNOHANG)); });
        ......
        if (!service) return true;
        // 进行退出的其他操作
        service->Reap(siginfo);
        ......
    }
    

    当接收到子进程的SIGCHLD信号后,会找出该进程对应的Service对象,然后调用Reap函数,我们看下函数内容:

    void Service::Reap(const siginfo_t& siginfo) {
        // 如果 不是oneshot 或者 是重启的子进程
        // 杀掉整个进程组,思考了下,先杀掉为重启做准备吧
        // 这样当重启的时候,就不会因为子进程已经存在而导致错误了
        if (!(flags_ & SVC_ONESHOT) || (flags_ & SVC_RESTART)) {
            KillProcessGroup(SIGKILL);
        }
        // 做一些当前进程的清理工作
        ......
    
        // Oneshot processes go into the disabled state on exit,
        // except when manually restarted.
        // 如果 是oneshot 或者 不是重启的子进程,设置为SVC_DISABLED
        if ((flags_ & SVC_ONESHOT) && !(flags_ & SVC_RESTART)) {
            flags_ |= SVC_DISABLED;
        }
    
        // Disabled and reset processes do not get restarted automatically.
        // 如果是SVC_DISABLED或者SVC_RESET
        // 设置进程状态为stopped,然后返回
        if (flags_ & (SVC_DISABLED | SVC_RESET))  {
            NotifyStateChange("stopped");
            return;
        }
    
        // If we crash > 4 times in 4 minutes, reboot into recovery.
        // 省略 crash 次数检测
        ......
        // 省略一些进程状态的设置,都是和重启相关的
        
        // Execute all onrestart commands for this service.
        // 执行onrestart的指令
        onrestart_.ExecuteAllCommands();
        
        // 设置状态为重启中
        NotifyStateChange("restarting");
        return;
    }
    

    Reap()函数中

    • 会根据对应进程Service对象的flags_标志位来判断该进程能不能重启
    • 如果需要重启,就给flags_标志位添加SVC_RESTARTING标志位

    到这里,我们清楚了handle_signal()函数的内部流程,那么它又是从哪里被调用的呢?

    我们再回到init.cppmain()方法中看看:

        while (true) {
            ......
            epoll_event ev;
            int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, epoll_timeout_ms));
            if (nr == -1) {
                PLOG(ERROR) << "epoll_wait failed";
            } else if (nr == 1) {
                ((void (*)()) ev.data.ptr)();
            }
        }
    

    请注意ev.data.ptr,还记得register_epoll_handler()函数不

    void register_epoll_handler(int fd, void (*fn)()) {
        ......
        ev.data.ptr = reinterpret_cast<void*>(fn);
        ......
    }
    

    epoll_wait有数据接收到时,就会执行((void (*)()) ev.data.ptr)();,也就是我们的回调函数handle_signal()

    咳咳咳,到这里就把Init进程对子进程死亡通知的逻辑给梳理完了,源码一直在变,好在核心的逻辑没有变化,且看且珍惜吧,哈哈哈~

    属性系统

    简介

    属性Android系统中大量使用,用来保存系统设置或在进程间传递一些简单的信息

    • 每个属性属性名属性值组成
    • 属性名通常一长串以.分割的字符串,这些名称的前缀有特定含义,不能随便改动
    • 属性值只能是字符串

    Java层可以通过如下方法来获取和设置属性:

    //class android.os.SystemProperties
        @SystemApi
        public static String get(String key);
        @SystemApi
        public static String get(String key, String def);
        @hide
        public static void set(String key, String val);
    

    native层可以使用:

    android::base::GetProperty(key, "");
    android::base::SetProperty(key, val);
    

    对于系统中的每个进程来说:

    • 读取属性值对任何进程都是没有限制的,直接由本进程从共享区域中读取
    • 修改属性值则必须通过Init进程完成,同时Init进程还需要检查发起请求的进程是否具有相应的权限

    属性值修改成功后,Init进程会检查init.rc文件中是否已经定义了和该属性值匹配的trigger。如果有定义,则执行trigger下的命令。如:

    on property:ro.debuggable=1
        start console
    

    这个trigger的含义是:当属性ro.debuggable被设置为1,则执行命令start console,启动console

    Android系统级应用和底层模块非常依赖属性系统,常常依靠属性值来决定它们的行为。

    Android的系统设置程序中,很多功能的打开和关闭都是通过某个特定的系统属性值来控制。这也意味着随便改变属性值将会严重影响系统的运行,因此,对于属性值的修改必须要有特定的权限。对于权限的设定,现在统一由SELinux来控制。

    属性服务的启动流程

    我们先看下属性服务启动的整体流程:

    int main(int argc, char** argv) {
        ......
        // 属性服务初始化
        property_init();
        ......
        // 启动属性服务
        start_property_service();
        ......
    }
    

    init.cppmain()函数中,通过property_init();来对属性服务进行初始化

    void property_init() {
        // 创建一个文件夹,权限711,只有owner才可以设置
        mkdir("/dev/__properties__", S_IRWXU | S_IXGRP | S_IXOTH);
        // 读取一些属性文件,将属性值存储在一个集合中
        CreateSerializedPropertyInfo();
        if (__system_property_area_init()) {// 创建属性共享内存空间(这个函数是libc库的部分)
            LOG(FATAL) << "Failed to initialize property area";
        }
        if (!property_info_area.LoadDefaultPath()) {// 加载默认路径上的属性到共享区域
            LOG(FATAL) << "Failed to load serialized property info file";
        }
    }
    

    然后通过start_property_service()函数启动服务:

    void start_property_service() {
        // 省略SELinux相关操作
        ......
        property_set("ro.property_service.version", "2");
        // 创建prop service对应的socket,并返回socket fd
        property_set_fd = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, false, 0666, 0, 0, nullptr);
        // 省略创建失败的异常判断
        ......
        // 设置最大连接数量为8
        listen(property_set_fd, 8);
        // 注册epolls事件监听property_set_fd
        // 当监听到数据变化时,调用handle_property_set_fd函数
        register_epoll_handler(property_set_fd, handle_property_set_fd);
    }
    
    • socket描述符property_set_fd被创建后,用epoll来监听property_set_fd
    • property_set_fd有数据到来时,init进程将调用handle_property_set_fd()函数来进行处理

    我们再来看下handle_property_set_fd()函数

    static void handle_property_set_fd() {
        static constexpr uint32_t kDefaultSocketTimeout = 2000; /* ms */
        // 省略一些异常判断
        ......
        uint32_t cmd = 0;
        // 省略cmd读取操作和一些异常判断
        ......
        switch (cmd) {
        case PROP_MSG_SETPROP: {
            char prop_name[PROP_NAME_MAX];
            char prop_value[PROP_VALUE_MAX];
            // 省略字符数据的读取组装操作
            ......
            uint32_t result =
                HandlePropertySet(prop_name, prop_value, socket.source_context(), cr, &error);
            // 省略异常情况处理
            ......
            break;
          }
    
        case PROP_MSG_SETPROP2: {
            std::string name;
            std::string value;
            // 省略字符串数据的读取操作
            ......
            uint32_t result = HandlePropertySet(name, value, socket.source_context(), cr, &error);
            // 省略异常情况处理
            ......
            break;
          }
        default:
            LOG(ERROR) << "sys_prop: invalid command " << cmd;
            socket.SendUint32(PROP_ERROR_INVALID_CMD);
            break;
        }
    }
    

    Init进程在接收到设置属性的cmd后,会执行处理函数HandlePropertySet()

    uint32_t HandlePropertySet(const std::string& name, const std::string& value,
                               const std::string& source_context, 
                               const ucred& cr, std::string* error) {
        // 判断要设置的属性名称是否合法
        // 相当于命名规则检查
        if (!IsLegalPropertyName(name)) {
            // 不合法直接返回
            return PROP_ERROR_INVALID_NAME;
        }
        // 如果是ctl开头,说明是控制类属性
        if (StartsWith(name, "ctl.")) {
            // 检查是否具有对应的控制权限
            ......
            // 权限通过后执行对应的控制指令
            // 其实控制指令就简单的几个start/stop等,大家可以在深入阅读下这个函数
            HandleControlMessage(name.c_str() + 4, value, cr.pid);
            return PROP_SUCCESS;
        }
        const char* target_context = nullptr;
        const char* type = nullptr;
        // 获取要设置的属性的上下文和数据类型
        // 后面会对target_context和type进行比较判断
        property_info_area->GetPropertyInfo(name.c_str(), &target_context, &type);
        // 检查是否具有当前属性的set权限
        if (!CheckMacPerms(name, target_context, source_context.c_str(), cr)) {
            // 没有直接返回
            return PROP_ERROR_PERMISSION_DENIED;
        }
        // 对属性的类型和要写入数据的类型进行判断
        // 大家看看CheckType函数就明白了,其实只有一个string类型。。。。。
        if (type == nullptr || !CheckType(type, value)) {
            // 不合法,直接返回
            return PROP_ERROR_INVALID_VALUE;
        }
        // 如果是sys.powerctl属性,需要做一些特殊处理
        if (name == "sys.powerctl") {
            // 增加一些额外打印
            ......
        }
        if (name == "selinux.restorecon_recursive") {
            // 特殊属性,特殊处理
            return PropertySetAsync(name, value, RestoreconRecursiveAsync, error);
        }
        return PropertySet(name, value, error);
    }
    

    除了一些特殊的属性外,真正设置属性的函数是PropertySet

    static uint32_t PropertySet(const std::string& name, const std::string& value, std::string* error) {
        size_t valuelen = value.size();
        // 判断属性名是否合法
        if (!IsLegalPropertyName(name)) {
            *error = "Illegal property name";
            return PROP_ERROR_INVALID_NAME;
        }
        // 判断写入的数据长度是否合法
        // 判断属性是否为只读属性(ro)
        if (valuelen >= PROP_VALUE_MAX && !StartsWith(name, "ro.")) {
            *error = "Property value too long";
            return PROP_ERROR_INVALID_VALUE;
        }
        // 判断要写入数据的编码格式
        if (mbstowcs(nullptr, value.data(), 0) == static_cast<std::size_t>(-1)) {
            *error = "Value is not a UTF8 encoded string";
            return PROP_ERROR_INVALID_VALUE;
        }
        // 根据属性名获取系统中存放的属性对象
        prop_info* pi = (prop_info*) __system_property_find(name.c_str());
        if (pi != nullptr) {
            // ro.* properties are actually "write-once".
            // ro开头的属性只允许写入一次
            if (StartsWith(name, "ro.")) {
                *error = "Read-only property was already set";
                return PROP_ERROR_READ_ONLY_PROPERTY;
            }
            // 如果已经存在,并且不是只读属性,执行属性更新函数
            __system_property_update(pi, value.c_str(), valuelen);
        } else {
            // 如果系统中不存在属性,执行添加属性添加函数
            int rc = __system_property_add(name.c_str(), name.size(), value.c_str(), valuelen);
            if (rc < 0) {
                *error = "__system_property_add failed";
                return PROP_ERROR_SET_FAILED;
            }
        }
        // Don't write properties to disk until after we have read all default
        // properties to prevent them from being overwritten by default values.
        // 如果是持久化的属性,进行持久化处理
        if (persistent_properties_loaded && StartsWith(name, "persist.")) {
            WritePersistentProperty(name, value);
        }
        // 将属性的变更添加到Action队列中
        property_changed(name, value);
        return PROP_SUCCESS;
    }
    

    Init进程epoll属性的socket,等待和处理属性请求。

    • 如果有请求到来,则调用handle_property_set_fd来处理这个请求
    • handle_property_set_fd函数里,首先检查请求者的uid/gid看看是否有权限,如果有权限则调property_service.cpp中的PropertySet函数。

    PropertySet函数中

    • 它先查找就没有这个属性,如果找到,更改属性。如果找不到,则添加新属性。
    • 更改时还会判断是不是ro属性,如果是,则不能更改。
    • 如果是persist的话还会写到/data/property/<name>中。

    最后它会调property_changed函数,把事件挂到队列里

    • 如果有人注册这个属性的话(比如init.rcon property:ro.kernel.qemu=1),最终会触发它

    ueventdwatchdogd简介

    ueventd进程

    守护进程ueventd的主要作用是接收ueventd来创建和删除设备中dev目录下的设备节点

    ueventd进程和Init进程并不是一个进程,但是它们的二进制文件是相同的,只不过启动时参数不一样导致程序的执行流程不一样。

    init.rc文件中

    on early-init
        start ueventd
    
    ## Daemon processes to be run by init.
    ##
    service ueventd /sbin/ueventd
        class core
        critical
        seclabel u:r:ueventd:s0
        shutdown critical
    

    这样Init进程在执行action eraly-init是就会启动ueventd进程。

    watchdogd进程

    watchdogdueventd类型,都是独立于Init的进程,但是代码和Init进程在一起。watchdogd是用来配合硬件看门狗的。

    当一个硬件系统开启了watchdog功能,那么运行在这个硬件系统之上的软件必须在规定的时间间隔内向watchdog发送一个信号。这个行为简称为喂狗(feed dog),以免watchdog记时超时引发系统重起。

    现在的系统中很少有看到watchdogd进程了,不过这种模式还是很不错、值得借鉴的

    展开全文
  • ======= 日期 内核版本 架构 作者 GitHub CSDN 2016-0729 ...CFS负责处理普通非实时进程, 这类进程是我们linux中最普遍的进程1 前景回顾1.1 CFS调度算法CFS调度算法的思想理想状态下每个进程
    日期内核版本架构作者GitHubCSDN
    2016-0729Linux-4.6X86 & armgatiemeLinuxDeviceDriversLinux进程管理与调度

    CFS负责处理普通非实时进程, 这类进程是我们linux中最普遍的进程

    1 前景回顾


    1.1 CFS调度算法


    CFS调度算法的思想

    理想状态下每个进程都能获得相同的时间片,并且同时运行在CPU上,但实际上一个CPU同一时刻运行的进程只能有一个。也就是说,当一个进程占用CPU时,其他进程就必须等待。CFS为了实现公平,必须惩罚当前正在运行的进程,以使那些正在等待的进程下次被调度.

    1,2 进程的创建


    fork, vfork和clone的系统调用的入口地址分别是sys_fork, sys_vfork和sys_clone, 而他们的定义是依赖于体系结构的, 而他们最终都调用了_do_fork(linux-4.2之前的内核中是do_fork),在_do_fork中通过copy_process复制进程的信息,调用wake_up_new_task将子进程加入调度器中

    1. dup_task_struct中为其分配了新的堆栈

    2. 调用了sched_fork,将其置为TASK_RUNNING

    3. copy_thread(_tls)中将父进程的寄存器上下文复制给子进程,保证了父子进程的堆栈信息是一致的,

    4. 将ret_from_fork的地址设置为eip寄存器的值

    5. 为新进程分配并设置新的pid

    6. 最终子进程从ret_from_fork开始执行

    1.3 处理新进程


    前面讲解了CFS的很多信息

    信息描述
    负荷权重 load_weightCFS进程的负荷权重, 与进程的优先级相关, 优先级越高的进程, 负荷权重越高
    虚拟运行时间 vruntime虚拟运行时间是通过进程的实际运行时间和进程的权重(weight)计算出来的。在CFS调度器中,将进程优先级这个概念弱化,而是强调进程的权重。一个进程的权重越大,则说明这个进程更需要运行,因此它的虚拟运行时间就越小,这样被调度的机会就越大。而,CFS调度器中的权重在内核是对用户态进程的优先级nice值, 通过prio_to_weight数组进行nice值和权重的转换而计算出来的

    我们也讲解了CFS的很多进程操作

    信息函数描述
    进程入队/出队enqueue_task_fair/dequeue_task_fair向CFS的就读队列中添加删除进程
    选择最优进程(主调度器)pick_next_task_fair主调度器会按照如下顺序调度 schedule -> __schedule -> 全局pick_next_task

    全局的pick_next_task函数会从按照优先级遍历所有调度器类的pick_next_task函数, 去查找最优的那个进程, 当然因为大多数情况下, 系统中全是CFS调度的非实时进程, 因而linux内核也有一些优化的策略

    一般情况下选择红黑树中的最左进程left作为最优进程完成调度, 如果选出的进程正好是cfs_rq->skip需要跳过调度的那个进程, 则可能需要再检查红黑树的次左进程second, 同时由于curr进程不在红黑树中, 它可能比较饥渴, 将选择出进程的与curr进程进行择优选取, 同样last进程和next进程由于刚被唤醒, 可能比较饥饿, 优先调度他们能提高系统缓存的命中率
    周期性调度task_tick_fair周期性调度器的工作由scheduler_tick函数完成, 在scheduler_tick中周期性调度器通过调用curr进程所属调度器类sched_class的task_tick函数完成周期性调度的工作

    而entity_tick中则通过check_preempt_tick函数检查是否需要抢占当前进程curr, 如果发现curr进程已经运行了足够长的时间, 其他进程已经开始饥饿, 那么我们就需要通过resched_curr函数来设置重调度标识TIF_NEED_RESCHED, 此标志会提示系统在合适的时间进行调度

    下面我们到了最后一道工序, 完全公平调度器如何处理一个新创建的进程, 该工作由task_fork_fair函数来完成

    处理新进程


    我们对完全公平调度器需要考虑的最后一个操作, 创建新进程时的处理函数:task_fork_fair(早期的内核中对应是task_new_fair, 参见LKML-sched: Sanitize fork() handling

    place_entity设置新进程的虚拟运行时间


    该函数先用update_curr进行通常的统计量更新, 然后调用此前讨论过的place_entity设置调度实体se的虚拟运行时间

        /*  更新统计量  */
        update_curr(cfs_rq);
    
        if (curr)
            se->vruntime = curr->vruntime;
        /*  调整调度实体se的虚拟运行时间  */
        place_entity(cfs_rq, se, 1);

    我们可以看到, 此时调用place_entity时的initial参数设置为1, 以便用sched_vslice_add计算初始的虚拟运行时间vruntime, 内核以这种方式确定了进程在延迟周期中所占的时间份额, 并转换成虚拟运行时间. 这个是调度器最初向进程欠下的债务.

    关于place_entity函数, 我们之前在讲解CFS队列操作的时候已经讲的很详细了

    参见linux进程管理与调度之CFS入队出队操作

    设想一下子如果休眠进程的vruntime保持不变, 而其他运行进程的 vruntime一直在推进, 那么等到休眠进程终于唤醒的时候, 它的vruntime比别人小很多, 会使它获得长时间抢占CPU的优势, 其他进程就要饿死了. 这显然是另一种形式的不公平,因此CFS是这样做的:在休眠进程被唤醒时重新设置vruntime值,以min_vruntime值为基础,给予一定的补偿,但不能补偿太多. 这个重新设置其虚拟运行时间的工作就是就是通过place_entity来完成的, 另外新进程创建完成后, 也是通过place_entity完成其虚拟运行时间vruntime的设置的.

    其中place_entity函数通过第三个参数initial参数来标识新进程创建和进程睡眠后苏醒两种情况的

    在进程入队时enqueue_entity设置的initial参数为0, 参见kernel/sched/fair.c, line 3207

    在task_fork_fair时设置的initial参数为1, 参见kernel/sched/fair.c, line 8167

    sysctl_sched_child_runs_first控制子进程运行时机


    接下来可使用参数sysctl_sched_child_runs_first控制新建子进程是否应该在父进程之前运行. 这通常是有益的, 特别在子进程随后会执行exec系统调用的情况下. 该参数的默认设置是1, 但可以通过/proc/sys/kernel/sched_child_first修改, 代码如下所示

        /*  如果设置了sysctl_sched_child_runs_first期望se进程先运行
         *  但是se进行的虚拟运行时间却大于当前进程curr
         *  此时我们需要保证se的entity_key小于curr, 才能保证se先运行
         *  内核此处是通过swap(curr, se)的虚拟运行时间来完成的  */
        if (sysctl_sched_child_runs_first && curr && entity_before(curr, se))
        {
            /*
             * Upon rescheduling, sched_class::put_prev_task() will place
             * 'current' within the tree based on its new key value.
             */
            /*  由于curr的vruntime较小, 为了使se先运行, 交换两者的vruntime  */
            swap(curr->vruntime, se->vruntime);
            /*  设置重调度标识, 通知内核在合适的时间进行进程调度  */
            resched_curr(rq);
        }

    如果entity_before(curr, se), 则父进程curr的虚拟运行时间vruntime小于子进程se的虚拟运行时间, 即在红黑树中父进程curr更靠左(前), 这就意味着父进程将在子进程之前被调度. 这种情况下如果设置了sysctl_sched_child_runs_first标识, 这时候我们必须采取策略保证子进程先运行, 可以通过交换curlr和se的vruntime值, 来保证se进程(子进程)的vruntime小于curr.

    适应迁移的vruntime值


    在task_fork_fair函数的最后, 使用了一个小技巧, 通过place_entity计算出的基准虚拟运行时间, 减去了运行队列的min_vruntime.

        se->vruntime -= cfs_rq->min_vruntime;

    我们前面讲解place_entity的时候说到, 新创建的进程和睡眠后苏醒的进程为了保证他们的vruntime与系统中进程的vruntime差距不会太大, 会使用place_entity来设置其虚拟运行时间vruntime, 在place_entity中重新设置vruntime值,以cfs_rq->min_vruntime值为基础,给予一定的补偿,但不能补偿太多.这样由于休眠进程在唤醒时或者新进程创建完成后会获得vruntime的补偿,所以它在醒来和创建后有能力抢占CPU是大概率事件,这也是CFS调度算法的本意,即保证交互式进程的响应速度,因为交互式进程等待用户输入会频繁休眠

    但是这样子也会有一个问题, 我们是以某个cfs就绪队列的min_vruntime值为基础来设定的, 在多CPU的系统上,不同的CPU的负载不一样,有的CPU更忙一些,而每个CPU都有自己的运行队列,每个队列中的进程的vruntime也走得有快有慢,比如我们对比每个运行队列的min_vruntime值,都会有不同, 如果一个进程从min_vruntime更小的CPU (A) 上迁移到min_vruntime更大的CPU (B) 上,可能就会占便宜了,因为CPU (B) 的运行队列中进程的vruntime普遍比较大,迁移过来的进程就会获得更多的CPU时间片。这显然不太公平

    同样的问题出现在刚创建的进程上, 还没有投入运行, 没有加入到某个就绪队列中, 它以某个就绪队列的min_vruntime为基准设置了虚拟运行时间, 但是进程不一定在当前CPU上运行, 即新创建的进程应该是可以被迁移的.

    CFS是这样做的:

    • 当进程从一个CPU的运行队列中出来 (dequeue_entity) 的时候,它的vruntime要减去队列的min_vruntime值

    • 而当进程加入另一个CPU的运行队列 ( enqueue_entiry) 时,它的vruntime要加上该队列的min_vruntime值

    • 当进程刚刚创建以某个cfs_rq的min_vruntime为基准设置其虚拟运行时间后,也要减去队列的min_vruntime值

    这样,进程从一个CPU迁移到另一个CPU之后,vruntime保持相对公平。

    参照sched: Remove the cfs_rq dependency
    from set_task_cpu()

    To prevent boost or penalty in the new cfs_rq caused by delta min_vruntime between the two cfs_rqs, we skip vruntime adjustment.

    减去min_vruntime的情况如下

    dequeue_entity():
    
        if (!(flags & DEQUEUE_SLEEP))
            se->vruntime -= cfs_rq->min_vruntime;
    
    task_fork_fair():
    
        se->vruntime -= cfs_rq->min_vruntime;
    
    switched_from_fair():
        if (!se->on_rq && p->state != TASK_RUNNING) 
        {
            /*
             * Fix up our vruntime so that the current sleep doesn't
             * cause 'unlimited' sleep bonus.
             */
            place_entity(cfs_rq, se, 0);
            se->vruntime -= cfs_rq->min_vruntime;
        }

    加上min_vruntime的情形

    enqueue_entity:
    // http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L3196
    
        if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
            se->vruntime += cfs_rq->min_vruntime;
    
    attach_task_cfs_rq:
    // http://lxr.free-electrons.com/source/kernel/sched/fair.c?v=4.6#L8267
    
    if (!vruntime_normalized(p))
            se->vruntime += cfs_rq->min_vruntime;
    展开全文
  • Android上层如何fork一个进程

    千次阅读 2017-09-23 12:06:44
    Android上层如何调用一个底层函数1. 背景本文讲的是调用流程,如何找到相应代码位置,更多的是一种分析代码的方式。此处将从ZygoteInit调用Zygote.forkSystemServer函数开始跟踪代码直到fork根据父进程和子进程返回...
  • 日期 内核版本 架构 作者 GitHub CSDN 2016-07-29 ...CFS负责处理普通非实时进程, 这类进程是我们linux中最普遍的进程1 前景回顾1.1 CFS调度算法CFS调度算法的思想理想状态下每个进程都能获得相同的时间
  • Android应对进程被杀死--Service(

    千次阅读 2017-01-19 11:42:23
    序言最近项目要实现这样一个效果:运行后,要有一个service始终保持在后台运行,不管用户作出什么操作,都要保证service不kill,这可真是一个难题。参考了现今各种定制版的系统和安全厂商牛虻软件,如何能保证自己...
  • Linux进程管理(进程数据结构

    千次阅读 2019-10-27 16:59:46
    Linux进程管理(一)进程数据结构 ...Linux内核中使用 task_struct 结构来表示一个进程,这个结构体保存了进程的所有信息,所以它非常庞大,在讲解Linux内核的进程管理,我们有必要先分析这个 task_struct 中的各...
  • 进程调度

    千次阅读 2012-12-04 15:27:35
    这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。 目录 进程有四个基本属性进程的三种基本状态处理机调度的分级进程调度的方式 非剥夺方式...
  • Android service进程保护

    万次阅读 2016-05-11 14:16:23
    什么才叫应用进程保活应用进程保活可以理解为应用位于后台永远不能杀死。这里的可以简略地分为两种情况,第种是当系统资源紧俏的时候或者基于某种系统自身的后台运行规则选择杀死你的后台应用来获得更多的资源,...
  • linux进程状态详解

    千次阅读 2016-12-08 15:45:14
    Linux是一个多用户,多任务的系统,可以...而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程
  • 进程,是对正在运行的程序的一个抽象。 进程 进程由三部分组成 程序段 相关的数据段 PCB ——进程控制块,即 PCB(Process Control Block) CPU由一个进程快速切换至另一个进程,使得每个进程运行几十或几...
  •  3、有一个 task_struct 数据结构记录进程的信息  4、独立的存储空间,意味着除了专用的系统空间堆栈外还要有用户空间堆栈  如果以上条件,只是缺少用户空间堆栈,完全没有称为“内核线程”,共享用户空间...
  • 进程保活

    千次阅读 2020-06-12 20:30:56
    进程保活的关键点有两个,一个进程优先级的理解,优先级越高存活几率越大。二是弄清楚哪些场景会导致进程会 kill,然后采取下面的策略对各种场景进行优化: 1. 提高进程的优先级 2. 在进程被 kill 之后能够唤醒 进程...
  • Oracle 后台进程详解

    万次阅读 2016-04-29 14:27:00
    后台进程后台进程负责保证数据库的稳定工作,每当数据库启动时,这些后台进程会自动启动,并且持续整 个实例的生命周期,每个进程负责一个独特的任务,表2-4 是一些最重要的后台进程。进 程缩 写描 述Database ...
  • Linux 进程调度浅析

    千次阅读 2015-04-09 20:30:19
    有人说,进程调度是操作系统中最为重要的一个部分。我觉得这种说法说得太绝对了一点,就像很多人动辄就说“某某函数比某某函数效率高XX倍”一样,脱离了实际环境,这些结论是比较片面的。  而进程调度究竟有多...
  • 进程

    2011-07-09 15:06:19
    <!-- @page { margin: 2cm } P { margin-bottom: 0.21cm } --> 第三章进程进程是任何多道程序设计的操作系统中的...通常把进程定义为程序执行的一个实例,因此,如果16个用户同时运行vi,那么就有16个独立的进程
  • 进程控制

    千次阅读 2016-07-02 21:19:27
    8 进程控制 8.1 简介 进程控制,主要包括创建新进程、执行程序和进程...每个进程都有一个非负整数标识的唯一进程ID。因为进程ID标识符总是唯一的,常将其用于其他标识符的一部分以保证其唯一性。如应用程序有时就把进
  • 进程冻结(freezing of task)

    千次阅读 2019-11-21 15:00:32
    进程冻结是当系统hibernate或者suspend时,对进程进行暂停挂起的种机制,后面主要以hibernate为例进行介绍。那么为什么要在hibernate或者suspend时需要把进程冻结呢?主要是出于如下的原因: 第1点,防止文件系统...
  • linux进程环境及进程属性

    千次阅读 2015-08-25 21:12:29
    每个进程都有一个独立的进程控制块(PCB)来管理每个进程资源。 进程的资源分为两大部分:内核空间进程资源 和 用户空间进程资源。 其中,内核空间进程资源是指:PCB相关信息,即进程PID、PPID、UID等,包括...
  •  向进程发送一个SIGSTOP信号,它就会因响应该信号而进入TASK_STOPPED状态(除非该进程本身处于TASK_UNINTERRUPTIBLE状态而不响应信号)。(SIGSTOP与SIGKILL信号一样,是非常强制的。不允许用户进程通过signal系列...
  • Android 多进程开发

    千次阅读 2017-12-07 18:47:31
     正常情况下,一个apk启动后只会运行在一个进程中,其进程名为AndroidManifest.xml文件中指定的应用包名,所有的基本组件都会在这个进程中运行。但是如果需要将某些组件(如Service、Activity等)运行在单独的进程...
  • 进程进程状态

    千次阅读 2017-04-18 10:06:40
    概念:执行中的程序,可执行的,从磁盘加载到主存,获得CPU一个静态的程序(头文件、代码段、数据段组成)通过操作系统,在内存中让CPU执行起来形成一个动态的执行过程,这个执行过程我们称之为进程 进程与程序的...
  • 进程管理

    千次阅读 多人点赞 2021-05-05 22:15:07
    进程管理
  • multiprocessing 是 Python 内置的标准进程模块,可运行于 Unix 和 Windows 平台台上。依赖于该模块,程序员得以充分利用机器上的多核资源。为便于使用,multiprocessing 模块提供了和 threading 线程模块相似 API。...
  • Java线程等待唤醒机制(加深理解)

    万次阅读 多人点赞 2019-08-04 16:28:06
    今天看源码的时候遇到这样一个场景,某...下面代码是一个简单的线程唤醒机制示例,主要就是在Activity启动的时候初始化并start线程,线程start后会进入等待状态,在onResume方法中执行notify方法唤醒线程。通过这样...
  • 进程的disk sleep状态与僵尸进程

    万次阅读 2015-09-08 09:25:03
    Linux进程有两种睡眠状态,种interruptible sleep,处在这种睡眠状态的进程是可以通过给它发信号来唤醒的,也是可以kill的,进程状态如下 [root@lmxe:/home]#cat /proc/949/status Name: sysmgt State: S ...
  • linux进程状态浅析

    千次阅读 2013-05-11 15:41:36
     在linux系统中,每个被运行的程序实例对应一个或多个进程。linux内核需要对这些进程进行管理,以使它们在系统中“同时”运行。linux内核对进程的这种管理分两个方面:进程状态管理,和进程调度。本文主要介绍进程...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 34,161
精华内容 13,664
关键字:

一个进程被唤醒意味着