精华内容
下载资源
问答
  • 一则来自 oppo 架设部的招聘
    千次阅读
    2022-03-24 00:47:49

    oppo 软件架设部 招聘各个方面的专家和工程师,我许多朋友都在这个部门,由于与本公众号的关注者契合度比较高,所以帮他们发一个招聘,Base 在深圳、南京、上海、成都、台北都可

    感兴趣的小伙伴可以通过下面几个渠道投递简历或者咨询

    1. 架设部提供的邮箱,标题中注明:姓名岗位名称工作地点,可以在正文中标注来源:AndroidPerformance 公众号,会得到优先处理、简历反馈、面试反馈等

      1. jintao@oppo.com

      2. liuqiong@oppo.com

    2. 不想直接投递简历,想先深入了解一下工作内容的小伙伴,可以加我微信(553000664)咨询或者内推,我会直接拉 oppo 架设部的人一起答疑解惑

    感兴趣的可以聊聊看,不一定要去,这一波 oppo 招聘还是很诚心的

    oppo 架设部,负责构建技术体系,牵引与培育技术核心,支撑技术竞争力发展等重要使命。目前聚集了一大批行业内顶尖的软件研发和架构设计的专家人才,接下来将持续发挥在软件解决方案设计、技术规划和工具效能提升、大数据等不同领域的能力。

    大家接触比较多的 oppo 技术输出应该是来自内核团队的 内核工匠,每周五都会输出一篇质量很高的文章

    eac6fd2b8e5c50b04731048a67659d82.png

    更多相关内容
  • 在上一部分,我们讨论了最基本常见的几类同步机制,这一部分我们将讨论相对复杂的几种同步机制,尤其是读写信号量和RCU,在操作系统内核中有相当广泛的应用。读写信号量(rw_semaphore...

    上一部分,我们讨论了最基本常见的几类同步机制,这一部分我们将讨论相对复杂的几种同步机制,尤其是读写信号量和RCU,在操作系统内核中有相当广泛的应用。

    • 读写信号量(rw_semaphore)

    • BKL(Big Kernel Lock,只包含在2.4内核中,不讲)

    • Rwlock

    • brlock(只包含在2.4内核中,不讲)

    • RCU(只包含在2.6内核及以后的版本中)

    一、读写信号量(RW_Semaphore)

    读写信号量与信号量有相似也有不同,它是如下一种同步机制:读写信号量将访问者分为读者或者写者,读者在持有读写信号量期间只能对该信号量保护的共享资源进行读访问,而只要一个任务需要写,它就被归类为写者,其进行访问之前必先获得写者身份,在其不需写访问时可降级为读者。读写信号量可同时拥有不受限的读者数,写者是排他性的,独占性的,而读者不排他。若读写信号量未被写者持有或者等待,读者就可以获得读写信号量,否则必须等待直到写者释放读写信号量为止;若读写信号量没有被读者或写者持有,也没用写者等待,写者可以获得该读写信号量,否则等待至信号量全部释放(没有其他访问者)为止。

    Structure Definition

    若从上述结构定义看,最关键的前三个字段与mutex、信号量十分相似不再赘述,后面的OSQ字段在Mutex中提起过。由于内核有关读写信号量的实现有两种,取决于CONFIG_RWSEM_GENERIC_SPINLOCK的配置,但是一般默认该配置是关的,因此选用默认版本的实现进行解读。读写信号量同mutex一样,在最近的改进中均引入了OSQ lock机制实现自旋等待。

    读写信号量与信号量之间的关系

    读写信号量可能会引起进程阻塞,但是它允许N个读执行单元同时访问共享资源,而最多只允许有一个写执行单元访问共享资源;因此,读写信号量是一种相对放宽条件的、粒度稍大于信号量的互斥机制。信号量不允许任何操作之间有并发,即:读操作与读操作之间、读操作与写操作之间、写操作与写操作之间,都不允许并发;而读写信号量则只允许读操作与读操作之间的并发,但不允许读操作与写操作之间的并发,也不允许写操作与写操作之间的并发。因此读写信号量比较适合读多写少的情况,可良好地利用读者并发的特性。

    Count 字段在读写信号量的表示含义

    读写信号量中的count字段并不如信号量一般表示可用资源数量,而是标记了当前的访问情况,我们取32位的情况分析,默认是取32位配置。

    先观察如下宏常量:

    然后我们再考虑count,我们发现均是上述宏组合的结果,可以归类为以下几种情况:

    所以可见count可以标记并区分许多访问情况, 尤其是当存在写者或阻塞时,其对应的有符号数(atomic_long_t)均为负数,可以作为判断的标记。

    在传统的读写信号量中,会直接进阻塞,因此只有等待队列非空还是为空的问题,但是在最近的改进中存在自旋等待的问题,因此使得在锁的获取中可能出现自旋状态的写者偷出锁的情况。

    __down_read & __up_read

    根据count字段的含义,count + 1小于0说明原本存在写者或者等待队列非空,因此不能获得锁,rwsem_down_read_failed调用

    一个读者释放后count - 1小于-1说明等待队列非空,因此还需唤醒等待的写者

    Rwsem_down_read不能直接获取时调用,首先判断等待队列是否为空,为空则字段置为非空,并将count回退之前读的尝试,将当前task压入等待队列,如果当前没有人持有或正在获取锁锁,则唤醒等待队列的前面的进程,同时将唤醒进程的waiter.task置NULL,在调度中若发现自己的waiter.task为NULL,说明轮到本进程运行,置为TASK_RUNNING

    down_write & up_write

    一个写者获取锁后,如果返回的count不是0xffff0001,那么写者获取信号量失败

    Rwsem_down_write_failed的基本逻辑与read相似,回退先前count的变化,对waitlist的处理,等待获取锁,有兴趣可以自己阅读源码。

    一个写者释放锁后,如果count返回小于0,说明等待非空,将其唤醒。

    RW_Semaphore  API

    二、读写锁(rw_lock)

    读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

    在读写锁保持期间也是抢占失效的。如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。

    Structure Definition

    从结构上看,读写锁与自旋锁基本相似,实际上二者的实现也十分相似,二者的关系可以类比读写信号量与信号量的关系。

    arch_read_lock & arch_read_unlock

    Read_lock实现上判断lock+1是否为负,为负说明有写者持有锁(0x80000000),此时调用wfe进入一小段自旋状态后再度执行;若非负,则将lock+1更新至lock中。

    对应read_lock,read_unlock仅仅需要将lock -1 更新至lock。

    arch_write_lock & arch_write_unlock

    write_lock 在尝试获得锁时,检查lock是否为0,不为0则说明有读者或者写者持有锁,此时wfe进入一小段等待直到lock为0,若lock为0则赋值lock获得锁。

    Write_unlock只需将lock置零即可。

    从这里可以看出,读写锁的实现上以及功能上,相当于针对自旋锁对于读多写少的场景提高并发度,设计原理与读写信号量十分类似。

    RW_Lock API

    三、顺序锁(seqlock)

    顺序锁是对读写锁的一种优化:读者绝不会被写者阻塞,也就说,读者可以在写者对被顺序锁保护的共享资源进行写操作时仍然可以继续读,不必等待写者完成写操作,写者也不需要等待所有读者完成读操作才去进行写操作。但是,写者与写者之间仍然是互斥的。写操作的优先级大于读操作。

    顺序锁有一个限制,它必须要求被保护的共享资源不含有指针,因为写者可能使得指针失效,但读者如果正要访问该指针,将导致OOPs。如果读者在读操作期间,写者已经发生了写操作,那么,读者必须重新读取数据,以便确保得到的数据是完整的。顺序锁适用于读多写少的情况。

    这种锁对于读写同时进行的概率比较小的情况,性能是非常好的,而且它允许读写同时进行,更大地提高了并发性。顺序锁的一个典型的应用在于时钟系统。

    Structure Definition

    从结构上看,也是依赖于自旋锁的,seqcount用于同步写者访问的顺序以更新读者访问,自旋锁的作用在于实现写操作之间的互斥,读者访问不受限制。

    write_seqlock & write_sequnlock

    顺序锁对写操作之间必须互斥,实现上调用spin_lock进行互斥,另外对seqcount操作以同步读者的访问。

    seqcount的计数符合以下规则:进入临界区时加一,离开临界区时也加一

    read_seqretry & read_seqbegin

    read_seqcount_begin返回当前seqlock的seqcount, 在读完后,需调用read_seqretry查看读者读完后的seqcount是否与读之前一致,一致则结束,不一致则说明有写操作正在或已经执行,需要重新读一次以更新数据。另外read_seqbegin返回的是lock.seqcount/2,实际上是写操作发生的次数。

    seqlock API

    其他_irqsave,_irq,_bh版本均是与其他锁类似的。

    四、RCU(Read-Copy Update)

    RCU是读写锁的高性能版本,既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。

    对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机(所有引用该数据的CPU都退出对共享数据的操作时)把指向原来数据的指针重新指向新的被修改的数据。有一个专门的垃圾收集器探测读者的信号,一旦所有读者都已发送信号告知它们不在使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。

    RCU不能替代读写锁,当写比较多时,对读者的性能提高不能弥补写者导致的损失,但是大多数情况下RCU的表现高于读写锁。

    RCU Basic API

    RCU 临界区管理

    之前的同步机制中,均是利用锁或原子操作实现的,一个锁管理一个临界区,并通过加锁解锁控制进程进入或者离开临界区。一个程序中可以存在若干的临界区,因此可以对应存在若干把锁分别管理,这是之前所有锁机制的基础。

    然而RCU并不基于锁机制实现,RCU字段是耦合在进程描述符和CPU变量中的,是一种与系统强耦合的同步机制,RCU负责管理进程内所有的临界区,进程通过调用rcu_read_lock与rcu_read_unlock标记读者临界区,通过rcu_assign_pointer、list_add_rcu将数据纳入保护区,当写者copy出新数据时在读者全部退出临界区后,将新数据指针更新,旧数据将在垃圾收集器的检查中被释放,但存在延迟。

    RCU 限制条件

    • RCU只保护动态分配并通过指针引用的数据结构

    • 在被RCU保护的临界区中,任何内核路径都不能睡眠(经典实现中)

    RCU callback的实现

    rcu_head 是RCU回调函数的关键结构。此外,回调机制主要涉及两个基本函数__call_rcu(用于注册), __rcu_reclaim(用于调用)。

    __call_rcu仅仅将func注册进rcu_head, 便立刻返回。该func一般用于回收释放copy后遗留的旧数据垃圾,但是RCU采用了延时执行防止读者还在读旧数据时回收数据造成崩溃。

    Rcp主要用于全局控制,而rcu的回调函数以链式组织,next用于遍历链。

    __rcu_reclaim用于回收rcu先前分配的旧数据,回调函数也是回收操作的一种。

    实际上,synchronize_rcu在等待读者全数退出临界区时,也通过call_rcu注册了回调函数。

    相对麻烦的是回收阶段,RCU通过一个垃圾收集器检查需要回收的旧数据并调用回调函数释放,准确的说调用rcu_check_callbacks检查是否有需要执行的回调函数,而后调用rcu_process_callbacks执行必要的rcu 回调函数。

    那么问题来了,谁去调用rcu_check_callbacks函数呢?时钟系统,每当时间片消耗完或者出现时钟中断,时钟系统都将调用rcu_check_callbacks进行及时检查处理,避免过量的旧数据垃圾造成内存浪费。

    RCU read

    rcu_read_lock与rcu_read_unlock的经典实现是不可抢占的,从代码看,这两个函数仅仅用于开关抢占。

    RCU read之所以禁止抢占,主要是由于写者必须等待读者完全执行完退出临界区方能修改数据指针。一旦读者被抢占,那么其退出临界区的过程将会阻塞,进而阻塞写者,这对性能是一种不小的开销。但是现在的linux 内核版本中提供了可抢占的版本,只是对抢占深度做了把控。

    RCU Synchronize

    可是RCU是如何获知所有读者已经离开临界区?RCU read实现中并没有设置字段标记进出临界区,RCU是通过什么判断的呢?既然RCU read过程不可抢占,那么换言之,若所有 CPU 都已经过一次上下文切换,则所有前置 reader 的临界区必定全部退出。

    我们主要分析以下两种:

    • rcu_check_callbacks

    • synchronize_rcu

    user其实在调用中真实的传入是user_tick,值为1指用户时间,0指系统时间,由于RCU必须在内核态执行,因此user为1说明必然不处于lock~unlock的时段,很有可能已经发生过rcu_read,因此发送一个RCU_SOFTIRQ软中断,调用rcu_process_callbacks。

    synchronize_rcu的核心是wait_rcu_gp函数。

    该函数通过注册一个func为wakeme_after_rcu的rcu_head并等待该rcu_head完成回调来判断之前的rcu读者已经全部退出。

    由于该rcu_head注册较晚,当且仅当当前的读者都已退出临界区,该rcu_head的回调才可能执行,因此当该func回调完成,就必然已经满足同步条件。最后销毁该多余的head内存。

    如下图:

    RCU Example

    Input.c 中的使用为例。

    Grab_device即挂载设备,注意这里的rcu_assign_pointer用于将dev->grab加入rcu保护的共享区,handle(处理函数)是其值。在这里完成了向rcu注册数据的过程。

    Input_pass_value处理所有的输入事件,首先我们read_lock标记进入临界区不可抢占,读出dev->grab并以处理输入事件,最后read_unlock退出临界区。

    Release device与挂载相对,释放过程即将原本的handler变为NULL, 最后调用synchronize_rcu通知所有输入事件handler移除

    五、同步机制之间的比较

    扫码关注
    “内核工匠”微信公众号
    Linux 内核黑科技 | 技术文章 | 精选教程
    展开全文
  • 本文出现的内核代码来自Linux5.10.61,为了减少篇幅,我们对引用的代码进行了删减(例如去掉了NUMA的代码,毕竟手机平台上我们暂时不关注这个特性),如果有兴趣,读者可以配合完整的源代码代码阅读本文。...

    前言

    我们描述CFS任务负载均衡的系列文章一共三篇,第一篇是框架部分,第二篇描述了task placement和active upmigration两个典型的负载均衡场景,第三篇是负载均衡的情景分析,包括tick balance、nohz idle balance和new idle balance。在负载均衡情景分析文档最后,我们给出了结论:tick balancing、nohz idle balancing、new idle balancing都是万法归宗,汇聚到load_balance函数来完成具体的负载均衡工作。本文就是第三篇负载均衡情景分析的附加篇,重点给大家展示load_balance函数的精妙。

    本文出现的内核代码来自Linux5.10.61,为了减少篇幅,我们对引用的代码进行了删减(例如去掉了NUMA的代码,毕竟手机平台上我们暂时不关注这个特性),如果有兴趣,读者可以配合完整的源代码代码阅读本文。

    一、概述

    本文主要分成三个部分,第一个部分就是本章,简单的描述了本文的结构和阅读前提条件。第二章是对load_balance函数设计的数据结构进行描述。这一章不需要阅读,只是在有需要的时候可以查阅几个主要数据结构的各个成员的具体功能。随后的若干个章节是以load_balance函数为主线,对各个逻辑过程进行逐行分析。

    需要强调的是本文不是独立成文的,很多负载均衡的基础知识(例如sched domain、sched group,什么是负载、运行负载、利用率utility,什么是均衡......)在CFS任务负载均衡系列文章的第一篇已经描述,如果没有阅读过,强烈建议提前阅读。如果已经具体负载均衡的基础概念,那么希望本文能够给你带来研读代码的快乐。

    二、load_balance函数使用的数据结构

    1、structlb_env

    在负载均衡的时候,通过 lb_env数据结构来表示本次负载均衡的上下文:

    50dbdc99fceb8a3dd004909dc2029f48.png

    2、structsd_lb_stats

    在负载均衡的时候,通过sd_lb_stats数据结构来表示scheddomain的负载统计信

    息:

    2cca34c9ba14e1e81a1d4d86dab6dd0e.png

    3、structsg_lb_stats

    在负载均衡的时候,通过sg_lb_stats数据结构来表示schedgroup的负载统计信息:

    7cf06b353ea09532baf6627bffc7ac6f.png

    4、structsched_group_capacity数据结构sched_group_capacity用来描述schedgroup的算力信息:

    4a560153d4415f51e2f556cc2cc60dce.png

    三、load_balance函数整体逻辑

    从本章开始我们进行代码分析,这一章是load_balance函数的整体逻辑,后面的章节都是对本章中的一些细节内容进行补充。load_balance函数实在是太长了,我们分段解读。第一段的逻辑如下:

    5a36d2a3468e30754a1303d9527a840f.png

    A、对load_balance函数的参数以及返回值解释如下:

    6f14177792e96530f9fbf910529407d9.png

    A、初始化本次负载均衡的上下文信息。具体可以参考对struct lb_env的解释。

    初始化完第一轮均衡的上下文,下面就看看具体的均衡操作为何。第二段的逻辑如下:

    1ea6d7ec8f42485d9269a5f893ff608e.png

    A、确定本轮负载均衡涉及的cpu,因为是第一轮均衡,所以所有的sched domain中的cpu都参与均衡(cpu_active_mask用来剔除无法参与均衡的CPU)。后续如果发现一些异常状况(例如由于affinity原因无法完成任务迁移),那么会清除选定的busiest cpu,跳转到redo进行全新一轮的均衡。

    B、判断env->dst_cpu这个CPU是否适合做指定scheddomain的均衡。如果被认定不适合发起balance,那么后续更高层level的均衡也不必进行了(设置continue_balancing等于0)。在base domain,每个group都只有一个CPU,因此所有的cpu都可以发起均衡。在non-base domain,每个group有多个CPU,如果每一个cpu都可以进行均衡,那么均衡就太密集了,白白消耗CPU资源,所以限制只有第一个idle的cpu可以发起均衡,如果没有idle的CPU,那么group中的第一个CPU可以发起均衡。

    C、在该sched domain中寻找最繁忙的schedgroup。具体逻辑后文会详细描述。如果没有找到busiest group,那么退出本level的均衡

    D、在最繁忙的sched group寻找最繁忙的CPU。具体逻辑后文会详细描述。如果没有找到busiest cpu,那么退出本level的均衡

    至此已经找到了source CPU,dest cpu就是发起均衡的thiscpu,那么就可以开始第一轮的任务迁移了,具体的代码逻辑如下:

    2550ea32281760f2709e7b72dd387375.png

    A、如果要从busiest cpu迁移任务到this cpu,那么至少要有可以拉取的任务。在拉取任务之前,我们先设定all pinned标志。当然后续如果发现不是all pinned的状况就会清除这个标志。

    B、为了达到sched domain的负载均衡,我们需要进行任务的迁移,因此我们这里需要遍历busiest rq上的任务,看看哪些任务最适合被迁移到this cpu rq。

    loop_max就是扫描src rq上runnable任务的次数。一般而言,任务迁移上限就是busiest runqueue上的任务个数,确保了每一个任务都被扫描到,但是一次均衡操作不适合迁移太多的任务(关中断区间太长),因此,即便busiest runqueue上的任务个数非常多,一次任务迁移不能大于sysctl_sched_nr_migrate个(目前设定是32个)。

    C、和redo不同,跳转到more_balance的新一轮迁移不需要寻找busiestcpu,只是继续扫描busiest rq上的任务列表,寻找适合迁移的任务。

    D、detach_tasks函数用来从busiestcpu的rq中摘取适合的任务。具体逻辑后面会详细描述。由于关中断时长的问题,detach_tasks函数也不会一次性把所有任务迁移到dest cpu上。

    E、将detach_tasks函数摘下的任务挂入到srcrq上去。由于detach_tasks、attach_tasks会进行多轮,ld_moved记录了总共迁移的任务数量,cur_ld_moved是本轮迁移的任务数

    F、在任务迁移过程中,src cpu的中断是关闭的,为了降低这个关中断时间,迁移大量任务的时候需要break一下。

    至此已经对dest rq上的任务列表完成了loop_max次扫描,要看情况是否要发起下一轮次的均衡。具体代码如下:

    4b3dc075cd067dbc7ba129ab73b40b77.png

    A、如果sched domain仍然未达均衡均衡状态,并且在之前的均衡过程中,有因为affinity的原因导致任务无法迁移到dest cpu,这时候要继续在src rq上搜索任务,迁移到备选的dest cpu,因此,这里再次发起均衡操作。这里的均衡上下文的dest cpu设定为备选的cpu,loop也被清零,重新开始扫描。

    B、本层次的sched domain因为affinity而无法达到均衡状态,我们需要把这个状态标记到上层sched domain的group中去,在上层sched domain进行均衡的时候,该group会被判定为group_imbalanced,从而有更大的机会选定为busiest group,从而解决该sched domain的均衡问题。

    C、如果选中的busiest cpu上的任务全部都是通过affinity锁定在了该cpu上,那么清除该cpu(为了确保下轮均衡不考虑该cpu),再次发起均衡。这种情况下,需要重新搜索source cpu,因此跳转到redo。

    至此,source rq上的cfs任务链表已经被遍历(也可能遍历多次),基本上对runnable 任务的扫描已经到位了,如果不行就只能考虑running task了,具体代码逻辑如下:

    2cc2e85c8bb330cde60a683994012737.png

    A、经过上面的一系列操作,没有完成任何任务的迁移,那么就需要累计sched domain的均衡失败次数。这个失败次数会导致后续进行更激进的均衡,例如迁移cache hot的任务、启动active balance。此外,这里过滤掉了new idle balance的失败,仅统计周期性均衡失败的次数,这是因为系统中new idle balance次数太多,累计其失败次数会导致nr_balance_failed过大,容易触发后续激进的均衡。

    B、判断是否要启动active balance。所谓activebalance就是把当前正在运行的任务迁移到dest cpu上。也就是说经过前面一番折腾,runnable的任务都无法迁移到dest cpu,从而达到均衡,那么就考虑当前正在运行的任务。

    C、在启动active balance之前,先看看busiestcpu上当前正在运行的任务是否可以运行在dest cpu上。如果不可以的话,那么不再试图执行均衡操作,跳转到out_one_pinned

    D、Busiest cpurunqueue上设置active balance的标记

    E、发起主动迁移

    F、完成了至少一个任务迁移,重置均衡失败计数

    Load_balance最后一段的程序逻辑主要是进行一些清理工作和设定balance_interval的工作,逻辑比较简单,不再详述,我们会在随后的章节中对load_balance函数中的一些过程做进一步的描述。

    四、寻找sched domain中最繁忙的group

    判断当前sched domain是否均衡并返回最忙group的功能是在find_busiest_group函数中完成的,我们分段来描述该函数的逻辑,我们先看第一段代码:

    5b0fe765c851e3bfa5f85f40f0913ff3.png

    A、负载信息都是不断的在变化,在寻找最繁忙group的时候,我们首先要更新scheddomain负载均衡信息,以便可以根据最新的负载情况来搜寻。

    update_sd_lb_stats会更新该sched domain上各个sched group的负载和算力,得到local group以及非local group最忙的那个group的均衡信息,以便后续给出最适合的均衡决策。具体的逻辑后面的章节会详述

    B、在系统没有进入overutilized状态之前,EAS起作用。如果EAS起作用,那么负载可能是不均衡的(考虑功耗),因此,这时候不进行负载均衡,依赖task placement的结果。

    update_sd_lb_stats函数找到了busiestgroup,结合local group的状态就可以判断系统的不均衡状态了。当然有一些比较容易判断的场景,具体代码如下:

    c462e4a4cc17d9e7e3523d12b07b6b18.png

    A、如果没有找到最忙的那个group,说明当前scheddomain中,其他的非local的最繁忙的group(后文称之busiest group)没有可以拉取到local group的任务,不需要均衡处理。

    B、Busiestgroup中有misfit task,那么必须要进行均衡,把misfit task拉取到local group中

    C、Busiestgroup是一个由于cpu affinity导致的不均衡,这个不均衡在底层sched domain无法处理。

    D、如果local group比busiestgroup还要忙,那么不需要进行均衡(目前的均衡只能从其他group拉任务到local group)

    其他的复杂场景需要进一步比拼local group和busiest group的情况,group_overloaded状态下判断是否均衡的代码如下:

    471ca5ae689ad97fd6207d67014e5192.png

    A、如果local group处于overloaded状态,那么需要通过avg_load的比拼来做均衡决策

    B、如果local group的平均负载比busiestgroup还要高,那么不需要进行均衡

    C、如果local group的平均负载高于scheddomain的平均负载,那么不需要进行均衡

    D、虽然busiest group的平均负载高于localgroup,但是高的不多,那也不需要进行均衡,毕竟均衡需要额外的开销。具体的门限是有sched domain的imbalance_pct确定的。

    非group_overloaded不看平均负载,主要看idlecpu的情况,具体代码如下:

    2e61c08de10ed5550f175f273a148364.png

    A、这里处理busiest group没有overload的场景,这时候说明该scheddomain中其他的group的算力都是cover当前的任务负载,是否要进行均衡,主要看idle cpu的情况

    B、反正busiest group当前算力能处理其runqueue上的任务,那么在本CPU繁忙的情况下没有必要进行均衡,因为这时候关注的是idle cpu,即让更多的idle cpu参与运算,因此,如果本CPU不是idle cpu,那么判断sched domain处于均衡状态。

    C、如果busiest group中有更多的idle CPU,那么也没有必要进行均衡

    D、如果busiest group中只有一个cfs任务,那么也没有必要进行均衡

    E、所有其他情况都是需要进行均衡。calculate_imbalance用来计算scheddomain中不均衡的状态是怎样的。

    具体如何进行均衡决策可以参考下面的表格:

    9202eb2c4d3b3fae178d58b6120cc5f7.png

    4abe49a525592d856a7f7d3fa9e6b872.png

    Balanced:该scheddomain处于均衡状态,不需要均衡。

    Force:该scheddomain处于不均衡状态,通过calculate_imbalance计算不均衡指数,并有可能通过任务迁移让系统进入均衡状态。

    Avg load:通过schedgroup的平均负载来判断是否需要均衡。尽量不均衡,除非非常的不均衡(通过sched domain的imbalance_pct参数来设定)

    Nr idle:dest cpu处于idle状态,并且localgroup的idle cpu个数大于busiest group的idle cpu个数,只有在这种情况下才进行均衡。

    五、更新sched domain的负载统计

    Sched domain的负载统计更新主要在update_sd_lb_stats函数中,其逻辑大致如下:

    b2ac7cccce2cdf6f0e8fa75ce1efa45e.png

    这一段主要是遍历该sched domain的所有group,对其负载统计进行更新。更新完负载之后,我们选定两个sched group:其一是local group,另外一个是最繁忙的non local group。具体逻辑过程解释如下:

    A、更新sched group的算力。在basedomain(在手机平台上就是MC domain)上,我们会更新发起均衡所在CPU的算力。注意:这里说的CPU算力指的是该CPU可以用于cfs任务的算力,即需要去掉由于thermal pressure而损失的算力,去掉RT/DL/IRQ消耗的算力。具体请参考update_cpu_capacity函数。在其他non-base domain(在手机平台上就是DIE domain)上,我们需要对本地sched group(包括发起均衡的CPU所在的group)进行算力更新。这个比较简单,就是把child domain(即MC domain)的所有sched group的算力加起来就OK了。更新后的算力保存在sched group中的sgc成员中。

    另外,更新算力没有必要更新的太频繁,这里做了两个限制:其一是只有local group才进行算力更新,其二是通过时间间隔来减少new idle频繁的更新算力。

    B、更新该sched group的负载统计,下面的章节会详细描述。

    C、在sched domain的各个group遍历中,我们需要两个group信息,一个是localgroup,另外一个就是non local group中的最忙的那个group。显然,如果是local group,不需要下面的比拼最忙的过程。

    D、找到non local group中的最忙的那个group。由于涉及各种grouptype,我们在下一章详述如何判断一个group更忙。

    E、更新sched domain上各个schedgroup总的负载和算力

    F、更新root domain的overload和overutil状态。对于顶层的scheddomain,我们需要把各个sched group的overload和overutil状态体现到root domain中。

    六、更新sched group的负载

    更新sched group负载是在update_sg_lb_stats函数中完成的,我们分段来描述该函数的逻辑,我们先看第一段代码:

    2716dd21ef8dc816ab7f28a61afb5b51.png

    A、sched group负载有三种,load、runnableload和util,把所有cpu上load、runnable load和util累计起来就是sched group的负载。除了PELT跟踪的load avg信息,我们还统计了sched group中的cfs任务和总任务数量。

    B、只要该sched group上有一个CPU上有1个以上的任务,那么就标记该schedgroup为overload状态。

    C、只要该sched group上有一个CPU处于overutilized(该cpu利用率已经达到cpu算力的80%),那么就标记该schedgroup为overutilized状态。

    D、统计该sched group中的idle cpu的个数

    E、当sched domain包括了算力不同的CPU(例如DIE domain),那么即便cpu上只有一个任务,但是如果该任务是misfit task那么也标记sched group为overload状态,并记录sched group中最大的misfit task load。需要注意的是:idle cpu不需要检测misfit task,此外,对于local group,也没有必要检测misfit task,毕竟同一个group,算力相同,不可能拉取misfit task到本cpu上。

    第二段代码如下:

    8f7b5635ebf9e9ff8e38d6507e763627.png

    A、更新sched group的总算力和cpu个数。再次强调一下,这里的capacity是指cpu可以用于cfs任务的算力

    B、判定sched group当前的负载状态

    C、计算sched group平均负载(仅在groupoverloaded状态才计算)。在overload的情况下,通过sched group平均负载可以识别更繁忙的group。

    sched group负载状态如下(按照负载从重到轻排列),括号中的数字是该group type的值,数值越大,载荷越重:

    dbd05bd2170f4cdb55b097ae6d6b29ec.png

     判断sched group繁忙程度的函数是group_classify,可以对照代码理解各schedgroup繁忙状态的含义。

    在对比sched group繁忙程度的时候,我们主要是对比group_type的值,大的值更忙,小的值比较闲,在相等的时候的判断规则如下:

    e1c2babc0ac63c6d1750f83676fceeda.png

    七、如何计算sched domain的不均衡程度

    一旦通过local group和busiestgroup的信息确定sched domain处于不均衡状态,我们就可以调用calculate_imbalance函数来计算通过什么方式(migrate task还是migrate load/util)来恢复sched domain的负载均衡状态,也就是设定均衡上下文的migration_type和imbalance 成员,下面我们分段来描述该函数的逻辑,我们先看第一段代码:

    1f68d3a712e5d8f2156ef4cdd553e345.png

    A、如果busiest group上有misfit task,那么优先对其进行misfit任务迁移,并且一次迁移一个misfittask。

    B、如果busiest group是因为cpuaffinity而导致的不均衡,那么通过通过迁移任务来达到平衡,并且一次迁移一个任务。

    上面的代码主要处理busiest group中的一些特殊情况,后面的代码主要分两段段来根据local group的状态来进行不均衡的计算。我们首先看local group有空闲算力的情况,我们分成两段分析,第一段代码如下:

    28ee67012a040be646b7843f7c155f31.png

    A、如果local group有一些空闲算力,那么我们还是争取把它利用起来,只要迁移的负载量既不overload local group,也不会让busiest group变得无事可做。

    B、如果sched domain标记了SD_SHARE_PKG_RESOURCES(MC domain),那么其在task placement的时候会尽量选择idlecpu。这里load balance路径需要和placement对齐:不使用空闲capacity而是使用nr_running来进行均衡。如果没有设置SD_SHARE_PKG_RESOURCES那么考虑使用migrate_util方式来达到均衡。

    C、如果local group有一些空闲算力,busiestgroup又处于繁忙状态(大于full busy),同时满足未设定SD_SHARE_PKG_RESOURCES(对于手机场景就是DIE domain,MC domain需要使用nr_running而不是util来进行均衡)。这种状态下,我们采用util来指导均衡,具体迁的utility设定为local group当前空闲的算力。

    D、有些场景下,local group的util大于其groupcapacity,根据步骤C计算的imbalance等于0(意味着不需要均衡)。然而,在这种场景下,如果local cpu处于idle状态,那么需要从busiest group迁移过来一个runnable task,从而确保了性能。

    Local gorup有空闲算力的第二段代码如下:

    e320a308716ccb45ad9854192189c1d8.png

    代码逻辑走到这里,说明busiest group也有空闲算力(local group也一样),这时候主要考虑的是任务的迁移,让sched domain中的idle cpu尽量的均衡。还有一种可能就是busiest group的状态是繁忙(大于fully busy),但是是在MC domain中进行均衡,这时候均衡的逻辑也是一样的看idle cpu。

    A、对于base domain(group只有一个CPU),我们还是希望任务散布在各个sched group(cpu)上。因此,这时候需要从busiest group中迁移任务,保证迁移之后,local group和busiest group中的任务数量相等。

    B、如果group中有多个CPU,那么我们的目标就是让localgroup和busiest group中的idle cpu的数量相等

    上面处理了local group有空闲算力的情况,下面的代码处理local group处于非group_has_spare状态的情况,代码如下:

    03eb2b7aa07e9d8430f9ca17d83ca3d7.png

    如果local group没有空闲算力,但是也没有overloaded,可以从busiest group迁移一些负载过来,但是这也许会导致local group进入overloaded状态。因此这里使用了avg_load来进一步确认是否进行负载迁移。具体的判断方法是local group的平均负载是否大于sched domain的平均负载。如果local group和busiest group都overloaded并且走入calculate imbalance,那么早就确认了busiest group的平均负载大于local group的平均负载。当local group或者busiest group都进入(或者即将进入)overloaded状态,这时候采用迁移负载的方式进行均衡,具体代码如下:

    da27ae783b1fbc05edfeb092db04db5e.png

    具体迁移的负载量是综合考虑localgroup、busiest group和sched domain的平均负载情况,确保迁移负载之和,local group、busiest group向sched domain的平均负载靠拢。

    八、如果寻找busiest group中最忙的CPU

    find_busiest_queue函数用来寻找busiestgroup中最繁忙的cpu。代码逻辑比较简单,和buiest group在上面判断的migrate type相关,不同的type使用不同的方法来寻找busiest cpu:

    dd8fc764c108dc7027d0d347bb8d85c0.png

    一旦找到最忙的CPU,那么任务迁移的目标和源头都确定了,后续就可以通过detach tasks和attach tasks进行任务迁移了。

    九、detach_tasks和attach_tasks

    至此,我们已经确定了从src cpu runqueue(即最繁忙的group中最繁忙的cpu)搬移若干load/util/task到dest cpu runqueue。不过无论是load还是util,最后还是要转成任务。detach_tasks就是确定具体从src rq迁移哪些任务,并把这些任务挂入lb_env->tasks链表中。detach_tasks函数第一段的代码逻辑如下:

    356ea7672bf61f81eb9f35c06c1f97cc.png

    A、src rq的cfs_tasks链表就是该队列上的全部cfs任务,detach_tasks函数的主要逻辑就是遍历这个cfs_tasks链表,找到最适合迁移到目标cpurq的任务,并挂入lb_env->tasks链表

    B、在idle balance的时候,没有必要把src上的唯一的task拉取到本cpu上,否则的话任务可能会在两个CPU上来回拉扯。

    C、从cfs_tasks链表队尾摘下一个任务。这个链表的头部是最近访问的任务。从尾部摘任务可以保证任务是cache cold的。

    D、当把dest rq上的任务都遍历过之后,或者当达到循环上限(sysctl_sched_nr_migrate)的时候退出循环。

    E、当dest rq上的任务数比较多的时候,并且需要迁移大量的任务才能完成均衡,为了减少关中断的区间,迁移需要分段进行(每sched_nr_migrate_break暂停一下),把大的临界区分成几个小的临界区,确保系统的延迟性能。

    F、如果该任务不适合迁移,那么将其移到cfs_tasks链表头部。

    上面对从cfs_tasks链表摘下的任务进行基本的判断,具体迁移该任务是否能达到均衡是由detach_tasks函数第二段代码逻辑完成的,具体如下:

    7d5d1df275014296549391f329b1a500.png

    A、计算该任务的负载。这里设定任务的最小负载是1。

    B、LB_MIN特性限制迁移小任务,如果LB_MIN等于true,那么task load小于16的任务将不参与负载均衡。目前LB_MIN系统缺省设置为false。

    C、不要迁移过多的load,确保迁移的load不大于env->imbalance。随着迁移错误次增加,这个限制可以适当放宽一些。

    D、对于migrate_util类型的迁移,我们通过任务的util和env->imbalance来判断是否迁移了足够的utility。需要注意的是这里使用了任务的estimation utilization。

    E、migrate_task类型的迁移不关注load或者utility,只关心迁移的任务数

    F、找到misfit task即完成迁移

    detach_tasks函数最后一段的代码逻辑如下:

    89bcda89200fc30e635e6fd6ca2d5a7e.png

    A、程序执行至此,说明任务P需要被迁移(不能迁移的都跳转到next符号了),因此需要从src rq上摘下,挂入env->tasks链表

    B、New idlebalance是调度延迟的主要来源,所有对于这种balance,我们一次只迁移一个任务

    C、如果完成迁移,那么就退出遍历src rq的cfs task链表。

    attach_tasks主要的逻辑就是遍历均衡上下文的tasks链表,摘下一个个的任务,挂入目标cpu的队列。

    十、如何判断一个任务是否可以迁移至目标CPU

    can_migrate_task函数用来判断一个任务是否可以迁移至目标CPU,具体代码逻辑如下:

    a73d905eb4d492d5780aaf83fe1c9458.png

    A、如果任务p所在的task group在src或者dest cpu上被限流了,那么不能迁移该任务,否者限流的逻辑会有问题

    B、Percpu的内核线程不能迁移

    C、任务由于affinity的原因不能在destcpu上运行,因此这里设置上LBF_SOME_PINNED标志,表示至少有一个任务由于affinity无法迁移

    D、下面的逻辑(E段)会设备备选目标CPU,如果是已经设定好了备选CPU那么直接返回,如果是newidle balance那么也不需要备选CPU,它的主要目标就是迁移一个任务到本idle的cpu。

    E、设定备选CPU,以便后续第二轮的均衡可以把任务迁移到备选CPU上

    can_migrate_task函数第二段代码逻辑如下

    c5dd72f8d01db742dbeac2000d382d96.png

    A、至少有一个任务是可以运行在dest cpu上(从affinity角度),因此清除allpinned标记

    B、正处于运行状态的任务不参与迁移,迁移runningtask是后续active migration的逻辑。

    C、判断该任务是否是cache-hot的,这主要从近期在srccpu上的执行时间点来判断,如果上次任务在src cpu上开始执行的时间比较久远(sysctl_sched_migration_cost是门限,目前设定0.5ms),那么其在cache中的内容大概率是被刷掉了,可以认为是cache-cold的。此外如果任务p是src cpu上的next buddy或者last buddy,那么任务是cache hot的。

    D、一般而言,我们只迁移cache cold的任务。但是如果进行了太多轮的尝试仍然未能让负载达到均衡,那么cache hot的任务也一样迁移。

    参考文献:

    1、内核源代码

    2、linux-5.10.61\Documentation\scheduler\*

    edfb9a17726fd4612223dc2b5f874d5e.gif

    长按关注

    内核工匠微信

    Linux 内核黑科技 | 技术文章 | 精选教程

    展开全文
  • Binder从入门到放弃(框架篇)

    千次阅读 2019-12-27 17:32:19
    前言 Binder从入门到放弃包括了... 参考文献: 1.Android系统源代码情景分析,罗升阳著 2.http://gityuan.com/tags/#binder,袁辉辉的博客 扫码关注 “内核工匠”微信公众号 Linux 内核黑科技 | 技术文章 | 精选教程

    前言

    Binder从入门到放弃包括了上下篇,上篇是框架部分,即本文。下篇通过几个典型的binder通信过程来呈现其实现细节,稍后发布,敬请期待。

    一、什么是Binder?

       Binder是安卓平台上的一种IPC framework,其整体的架构如下:

    Binder渗透到了安卓系统的各个软件层次:在应用层,利用Framework中的binder Java接口,开发者可以方便的申请系统服务提供的服务、实现自定义Service组件并开放给其他模块等。由于Native层的binder库使用的是C++,因此安卓框架中的Binder模块会通过JNI接口进入C/C++世界。在最底层,Linux内核提供了binder驱动,完成进程间通信的功能。

    Binder对安卓非常重要,绝大多数的进程通信都是通过Binder完成。Binder采用了C/S的通信形式:

    从进程角度看,参与Binder通信的实体有三个:binder client、binder server和service manager。Binder server中的service组件对外提供了服务,但是需要对外公布,因此它会向service manager注册自己的服务。Binder client想要请求服务的时候统一到service manager去查询,获取了对应的描述符后即可以通过该描述符和service组件进行通信。当然,这些IPC通信并不是直接在client、server和service manager之间进行的,而都是需要通过binder driver间接完成。

    安卓应用程序开发是基于组件的,也就是说通过四大组件(Activity、Service、Broadcast Receiver和Content Provider),开发者可以象搭积木一样的轻松开发应用程序,而无需关心底层实现。然而安卓这种面向对象的应用框架环境却是基于传统的Linux内核构建的,这使得安卓在进程间通信方面遇到了新的挑战,这也就是为何谷歌摒弃了传统的内核IPC机制(管道、命名管道、domain socket、UDP/TCP socket、system V IPC,share memory等),建立了全新的binder通信形式,具体细节我们下一章分解。

    二、为什么是Binder?

    在上一节中,我们简单的描述了binder的C/S通信模型,在内核已经提供了socket形态的C/S通信机制的情况下,在安卓系统上直接使用socket这种IPC机制似乎是顺理成章的,为何还要重新制作一个新的轮子呢?是否需要新建轮子其实是和需求相关的,下面我们会仔细分析安卓系统上,组件之间IPC机制的需求规格,从而窥视谷歌创建全新binder通信机制背后的原因。

    1、安卓系统需要的是一个IPC框架

    为了提高软件生产效率,安卓的应用框架希望能够模糊进程边界,即在A组件调用B组件的方法的时候,程序员不需要考虑是否跨进程。即便是在不同的进程中,对B组件的服务调用仍然象本地函数调用一样简单。传统Linux内核的IPC机制是无法满足这个需求的,安卓需要一个复杂的IPC framework能够支持线程池管理、自动跟踪引用计数等有挑战性的任务。

    当然,基于目前Linux内核的IPC机制,也可以构建复杂的IPC framework,不过传统的内核IPC机制并没有考虑面向对象的应用框架,因此很多地方实现起来有些水土不服。上图给了一个简单的例子:在一个地址空间中跟踪对象的引用计数非常简单,可以在该对象内部构建一个引用计数,每当有本进程对象引用service组件对象的时候,引用计数加一,不再引用的时候减一,没有任何对象引用service组件对象的时候,该对象可以被销毁。不过,当引用该service组件的代理对象来自其他进程空间(例如binder client的组件代理对象)的时候,事情就不那么简单了,这需要一个复杂的IPC framework来小心的维护组件对象的引用计数,否则在server端销毁了一个组件对象,而实际上有可能在client端还在远程调度该service组件提供的服务。

    为了解决这个问题,binder驱动构建了binder ref和binder node数据对象,分别对应到上层软件中的service组件代理和service组件对象,同时也设计了相应的binder通信协议来维护引用计数,解决了传统的IPC机制很难解决的跨进程对象生命周期问题。

    2、安卓系统需要的是高效IPC机制

    我们再看一下性能方面的需求:由于整个安卓系统的进程间通信量比较大,我们希望能有一个性能卓越的IPC机制。大部分传统IPC机制都需要两次拷贝容易产生性能问题。而binder只进行了一次拷贝,性能优于大部分的传统IPC机制,除了share memory。当然,从内存拷贝的角度看,share memory优于binder,但实际上如果基于share memory设计安卓的IPC framework,那么还是需要构建复杂的同步机制,这也会抵消share memory部分零拷贝带来性能优势,因此Binder并没有选择共享内存方案,而是在简单和性能之间进行了平衡。在binder机制下,具体的内存拷贝如下图所示:

    binder server会有专门二段用于binder通信的虚拟内存区间,一段在内核态,一段在用户空间。这两段虚拟地址空间映射到同样的物理地址上,当拷贝数据到binder server的内核态地址空间,实际上用户态也就可以直接访问了。当Binder client要把一个数据块传递到binder server(通过binder transaction)的时候,实际上会在binder server的内核虚拟地址空间中分配一块内存,并把binder client的用户地址空间的数据拷贝到binder server的内核空间。因为binder server的binder内存区域被同时映射到用户空间和内核空间,因此就可以省略一次数据考虑,提高了性能。

    并不是说安卓不使用共享内存机制,实际上当进程之间要传递大量的数据的时候(例如APP的图形数据要传递到surfaceflinger进行实际的显示)还是使用了share memory机制(Ashmem)。安卓使用文件描述符来标示一块匿名共享内存,binder机制可以把文件描述符从一个进程传递到另外的进程,完成文件的共享。一个简单的示意图如下:

    在上图中,binder client传递了fdx(binder client有效的描述符)到binder server,实际上binder驱动会通过既有的内核找到对应的file object对象,然后在binder server端找到一个空闲的fd y(binder server进程有效),让其和binder client指向同一个对象。这个binder client传递了fd x到binder server,在server端变成fd y并实现了和client进程中fd x指向同一个文件的目标。而传统的IPC机制(除了socket)没有这种机制。

    3、安卓系统需要的是稳定的IPC机制

    数据传输形态(非共享内存)的IPC机制有两种形态:byte stream和message-based。如果使用字节流形态的方式(例如PIPE或者socket),那么对于reader一侧,我们需要在内核构建一个ring buffer,把writer写入的数据拷贝到reader的这个环形缓冲区。而在reader一侧的,如何管理这个ring buffer是一个头疼的事情。因此binder采用了message-based的形态,并形成了如下的缓冲区管理方式:

    需要进行Binder通信的两个进程传递结构化的message数据,根据message的大小在内核分配同样大小的binder缓冲区(从binder内存区中分配,内核用binder alloc对象来抽象),并完成用户空间到内核空间的拷贝。Binder server在用户态的程序直接可以访问到binder buffer中的message数据。

    从内存管理的角度来看,这样的方案是一个稳定性比较高的方案。每个进程可以使用的binder内存是有限制的,一个进程不能使用超过1M的内存,杜绝了恶意APP无限制的通过IPC使用内存资源。此外,如果撰写APP的工程师不那么谨慎,有些传统的Linux IPC机制容易导致内存泄露,从而导致系统稳定性问题。同样的,如果对通信中的异常(例如server进程被杀掉)没有有良好的处理机制,也会造成稳定性问题。Binder通信机制提供了death-notification机制,优雅的处理了通信两端异常退出的异常,增强了系统的稳定性。

    4、安卓系统需要的是安全的IPC机制

    从安全性(以及稳定性)的角度,各个安卓应用在自己的sandbox中运行并用一个系统唯一的id来标示该应用(uid)。由于APP和系统服务进程是完全隔离的,安卓设计了transaction-based的进程间通信机制:binder,APP通过binder请求系统服务。由于binder driver隔离了通信的两段进程。因此实际上在binder driver中是最好的一个嵌入安全检查的地方,具体可以参考下面的安全检查机制示意图:

    安卓是一个开放的系统,因此安全性显得尤为重要。在安卓世界,uid用来标示一个应用,在内核(而非用户空间)中附加UID/PID标识并在具体提供服务的进程端进行安全检查,主要体现在下面两个方面:

      a.系统提供了唯一的上下文管理者:service manager并且只有信任的uid才能注册service组件。

      b.系统把特定的资源权限赋权给Binder server(service组件绑定的进程),当binder client请求服务的时候对uid进行安全检查。

    传统的IPC机制在内核态并不支持uid/pid的识别,通过上层的通信协议增加发起端的id并不安全,而且传统的IPC机制没有安全检查机制,这种情况下任何人都可以撰写恶意APP并通过IPC访问系统服务,获取用户隐私数据。

    解决了what和why之后,我们后续的章节将主要讲述binder的软件框架和通信框架,在了解了蓝图之后,我们再深入到binder是如何在各种场景下工作的。随着binder场景解析,我们也顺便描述了binder驱动中的主要数据结构。

    三、Binder软件框架和通信框架

      1、软件框架

    一个大概的软件结构如下:

    所有的通信协议都是分层的,binder也不例外,只不过简单一些。Binder通信主要有三层:应用层,IPC层,内核层。如果使用Java写应用,那么IPC层次要更丰富一些,需要通过Java layer、jni和Native IPC layer完成所有的IPC通信过程。如果使用C++在Native层写应用,那么基本上BpBinder和BBinder这样的Native IPC机制就足够了,这时候,软件结构退化成(后续我们基本上是基于这个软件结构描述):

    对于应用层而言,互相通信的实体交互的是类似start activity、add service这样的应用相关的协议数据,通信双方并不知道底层实现,感觉它们之间是直接通信似得。而实际上,应用层数据是通过Native IPC层、kerenl层的封装,解析,映射完成了最后的通信过程。在Native IPC层,BpBinder和BBinder之间通信之间的封包有自己的格式,IPC header会标记通信的起点和终点(binder ref或者binder node)、通信类型等信息,而应用层数据只是IPC层的payload。同样的,表面上是BpBinder和BBinder两个实体在交互IPC数据,实际上需要底层binder driver提供通信支持。

      2、通信框架

    分别位于binder client和server中的应用层实体进行数据交互的过程交过transaction,当然,为了保证binder transaction能够正确、稳定的完成,binder代理实体、binder实体以及binder driver之间需要进行非常复杂的操作,因此,binder通信定义了若干的通信协议码,下面表格列出了几个常用的binder实体或者binder代理实体发向binder driver的通信协议码:

    Binder command code

    描述

    BC_TRANSACTION

    Binder代理实体请求数据通信服务

    BC_REPLY

    Binder实体完成了服务请求的回应

    BC_INCREFS

    BC_DECREFS

    管理binder ref的引用计数

    ......

    ......

    下面的表格列出了几个常用的binder driver发向binder实体或者binder代理实体的通信协议码:

    Binder response code

    描述

    BR_TRANSACTION

    Binder driver收到transaction请求,将其转发给binder实体对象

    BR_REPLY

    Binder driver通知binder代理实体,server端已经完成服务请求,返回结果。

    BR_TRANSACTION_COMPLETE

    Binder driver通知binder代理实体,它发出的transaction请求已经收到。或者,Binder driver通知binder实体,它发出的transaction reply已经收到。

    ......

    ......

    Binder通信的形态很多种,有些只涉及binder server中的实体对象和binder driver的交互。例如:BC_REGISTER_LOOPER。不过使用最多、过程最复杂的还是传递应用数据的binder transaction过程,具体如下:

    Binder client和server之间的进程间通信实际上是通过binder driver中转的。在这样的通信框架中,client/server向binder driver发送transaction/reply是直接通过ioctl完成的,而相反的方向,binder driver向client/server发送的transaction/reply则有些复杂,毕竟在用户空间的client/server不可能不断的轮询接收数据。正因为如此,在binder通信中有了binder work的概念,具体的方式如下:

    对于binder transaction这个场景,Binder work对象是嵌入在transaction对象内的,binder driver在把transaction(服务请求)送达到target的时候需要做两个动作:

      a.选择一个合适的binder work链表把本transaction相关的work挂入链表。

      b.唤醒target process或者target thread

    对于异步binder通信,work是挂入binder node对应的work链表。如果是同步binder通信,那么要看是否能够找到空闲的binder thread,如果找到那么挂入线程的work todo list,否则挂入binder process的链表。

      3、应用层通信数据格式

    本身应用层的数据应该是通信两端的实体自己的事情,不过由于需要交互binder实体对象信息,因此这里也简单描述其数据格式,如下:

    Binder Client和server之间通信的基本单元是应用层的数据+相关的binder实体对象数据,这个基本的单元可以是1个或者多个。为了区分开各个基本的单元,在应用层数据缓冲区的尾部有一个数组保存了各个binder实体对象的位置。每一个binder实体用flat_binder_object来抽象,主要的成员包括:

    成员

    描述

    header

    说明该binder实体的类型,可能的类型包括:

    1. 本地binder实体对象

    2. 远端binder实体对象(handle)

    3. 文件描述符

    binder_uintptr_t binder

    描述本地binder实体对象

    __u32 handle

    描述远端binder实体对象

    binder_uintptr_t cookie

    描述本地binder实体对象

    我们这里可以举一个简单的例子:假设我们写了一个APP,实现了一个xxx服务组件,在向service manager注册的时候就需要发起一次transaction,这时候缓冲区的数据就包括了上面图片中的应用层数据和一个xxx服务组件对应的binder实体对象。这时候应用层数据中会包括“xxx service”这样的字符串信息,这是方便其他client可以通过这个字符串来寻址到本service组件必须要的信息。除了应用层数据之外,还需要传递xxx service组件对应的binder实体。上面的例子说的是注册service组件的场景,因此传递的是本地binder实体对象。如果场景切换成client端申请服务的场景,这时候没有本地对象,因此需要传递的是远端的binder实体对象,即handle。因此flat_binder_object描述的是transaction相关的binder实体对象,可能是本地的,也可能是远端的。

    4、Binder帧数据格式

    Binder IPC层的数据格式如下:

    Binder IPC层看到的帧数据单元是协议码+协议码数据,一个完整的帧数据是由一个或者多个帧数据单元组成。协议码区域就是上文中描述的BC_XXX和BR_XXX,不同的协议码有不同的协议码数据,同样的我们采用binder transaction为例说明协议码数据区域。BR_TRANSACTION、BR_REPLY、BC_TRANSACTION和BC_REPLY这四个协议码的数据都是binder_transaction_data,和应用层的数据关系如下:

    Binder transaction信息包括:本次通信的目的地、sender pid和uid等通用信息,此外还有一些成员描述应用层的数据buffer信息,具体大家可以参考源代码。顺便提一句的是这里的sender pid和uid都是内核态的binder driver附加的,用户态的程序无法自己标记,从而保证了通信的安全性。

    了解了整体框架之后,我们后面的章节将进入细节,通过几个典型binder通信场景的分析来加强对binder通信的理解,这些将在下篇文档中呈现,敬请期待!

    参考文献:

    1.Android系统源代码情景分析,罗升阳著

    2.http://gityuan.com/tags/#binder,袁辉辉的博客

    扫码关注
    “内核工匠”微信公众号
    Linux 内核黑科技 | 技术文章 | 精选教程
    展开全文
  • CFS任务的负载均衡(框架篇)

    千次阅读 2020-04-03 17:00:00
    我们描述负载均衡的系列文章一共三篇,第一篇是框架部分,即本文,主要描述了负载均衡相关的原理... linux-5.4.28\Documentation\scheduler\* 扫描关注 “内核工匠”微信公众号 Linux 内核黑科技 | 技术文章 | 精选教程
  • Binder从入门到放弃(细节篇)

    千次阅读 2020-01-17 20:35:34
    service manager会打开/dev/binder设备,一个进程打开binder设备就意味着该进程会使用binder这种IPC机制,这时候,在内核态会相应的构建一个binder proc对象,来管理该进程相关的binder资源(binder ref、binder ...
  • generic_file_direct_write调用了文件系统提供的direct IO函数,但如文件系统的其它函数一样,大部分文件系统的实现是封装的内核的direct io函数,以ext2为例,最终调用的还是do_blockdev_direct_IO,这个函数是读写...
  • Linux SPI 驱动

    千次阅读 2021-12-25 01:00:16
    一、SPI协议 SPI是英语Serial Peripheral ... 参考文献 1、https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/?h=v4.14.258 长按关注内核工匠微信 Linux 内核黑科技 | 技术文章 | 精选教程
  • linux IO Block layer 解析

    千次阅读 2020-03-20 17:00:00
    参考资料 [1] Linux Block IO: Introducing Multi-queue SSD Access on Multi-core Systems [2] https://kernelnewbies.org/Linux_5.0#Block_layer 扫描关注 “内核工匠”微信公众号 Linux 内核黑科技 | 技术文章 |...
  • SRCU的内核简单实现

    2020-02-19 22:53:01
    分析一个内核srcu锁的简单实现版本
  • 在之前的公众号文章中,我们介绍过File cache的定义,其读流程及写流程,想了解上篇详情的可阅读:Linux内核File cache机制(上篇)本篇则接上篇,主要介绍Linux mma...
  • 一、背景 我们常在Linux平台bash环境下执行一条cmd,如看下当前文件有哪些"ls -l"。这条cmd会fork一个新的进程,然后完成ls可执行程序的加载和执行。...“内核工匠”微信公众号 Linux 内核黑科技 | 技术文章 | 精选教程
  • 【摘 要】 创新是社会发展的内核,弘扬工匠精神是大趋势,培养幼儿专注、探索、合作,乐于动手和精益求精的精神势在必行,混龄班木工游戏是3-6岁幼儿混在一起,通过选择适宜自己的木工工具和材料,采用项目式团队合作的...
  • 最近一头扎进了 Linux 内核的学习中,Linux 内核的学习,需要的基础知识太多太多了:C 语言、汇编语言、数据结构与算法、操作系统原理、计算机组成原理、计算机体系结构。在囫囵吞枣补完一些计算机基础知识后,还是...
  • Linux source code (v5.15.11) - Bootlin Linux内核 在线源代码 查询函数定义和使用 Linux Kernel(Android) 加密算法总结(一)(cipher、compress、digest)_万能的终端和网络-程序员宅基地 - 程序员宅基地 ...
  • Linux内核内存检测工具KASAN

    千次阅读 2020-11-13 17:00:00
    KASAN 是 Kernel Address Sanitizer 的缩写,它是一个动态检测内存错误的工具,主要功能是检查内存越界访问和使用已释放的内存等问题。...“内核工匠”微信公众号 Linux 内核黑科技 | 技术文章 | 精选教程
  • 1. 内核同步引入 1.1 并发执行的原因 1.2 临界区和竞争条件 1.2.1 临界区 1.2.2 竞争条件 1.3 确定保护对象 1.4 临界区保护思路 1.5 死锁 1.5.1 死锁的概念 1.5.2 死锁的原因 1.5.3 如何避免死锁 2.内核...
  • cgroup原理

    2019-09-25 23:19:00
    https://www.ibm.com/developerworks/cn/linux/1506_cgroup/index.html 转载于:https://www.cnblogs.com/xingmuxin/p/11384086.html
  • Linux内核File cache机制(上篇)

    千次阅读 2020-12-18 17:04:50
     File cache概述 Linux File cache机制,每次动笔想写到该知识点的时候,我心里总会犹豫迟疑,众所周知内存管理是Linux系统的比较难啃的子系统之一,而内核文件缓存机制是内存管理框架中难度较大的知识点。...
  • Android之LCD屏驱动

    千次阅读 2012-07-06 20:04:02
    学习要点:1、RK3066平台中LCD驱动的架构; 2、如何配屏; 3、如何看屏参——包括象素时钟、行同步信号、场同步信号、行消隐时间、场消隐时间等; 4、用示波器看波形,跟规格书对比,检查时序是否正确;
  • android平台eBPF初探

    千次阅读 2021-01-08 17:00:00
    一、eBPF是什么 eBPF是extended BPF的缩写,而BPF是Berkeley Packet Filter的缩写。对linux网络比较熟悉的伙伴对BPF应该比较了解,它通过特定的语法规则使用...内核工匠微信 Linux 内核黑科技 | 技术文章 | 精选教程
  • Linux 内核中的位数组和位操作除了不同的基于链式和树的数据结构以外,Linux 内核也为位数组(或称为位图bitmap)提供了 API。位数组在 Linux 内核里被广泛使用,并且在以下的源代码文件中包含了与这样的结构搭配使用...
  • 本文发表于内核工匠公众号,旨在给内核开发的小伙伴分享:Android系统层面用户UI交互的设计,从而理解手机黑屏定屏时背后的故事。Android系统对黑屏定屏类问题的维测思路,有那些先进的思想,有那些改进的空间。如果...
  • kswapd介绍

    2022-04-09 01:08:18
    但是,由于内存是有限的,⽽APP可以打开更多,内存总有耗尽的时刻,所以内核对于这种内存紧张情况实现了⼀套内存回收机制,包括kswapd回收内存和direct reclaim,例如回收最近很少使⽤的pagecache,把anonpage swap...
  • 在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实像多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问,尤其是在多处理器系统上,更需要一些同步机制来同...
  • TEE原理及应用举例

    千次阅读 2021-03-06 00:30:13
    参考资料: [1]Global Platform specification and Technology Document [2]http://kernel.meizu.com/2017/08/17-33-25-tee_fp.html [3]https://www.jianshu.com/p/c238bfea3e46 长按关注 内核工匠微信 Linux 内核...
  • FUSE文件系统

    千次阅读 2020-10-30 17:00:00
    Fuse(filesystem in userspace),是一个用户空间的文件系统。通过fuse内核模块的支持,开发者只需要根据fuse提供的接口实现具体的文件操作就可以实现一个文件系...
  • GKI改造原则、机制和方法

    千次阅读 多人点赞 2021-01-29 17:00:00
    Google在android11-5.4分支上开始要求所有下游厂商使用Generic Kernel Image(GKI),需要将SoC和device相关的代码从核心内核剥离到可加载模块中...
  • KVM原理简介

    2021-09-25 00:58:06
    一、概述KVM的全称是Kernel-based Virtual Machine,其是一种基于linux内核的采用硬件辅助虚拟化技术的全虚拟化解决方案。它最初由以色列的初创公司Qumran...
  • 一、Linux固件子系统概述 固件是硬件设备自身执行的一段程序。... 参考文档: https://www.kernel.org/doc/html/v4.13/driver-api/firmware/index.html 长按关注 内核工匠微信 Linux 内核黑科技 | 技术文章 | 精选教程

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,069
精华内容 427
关键字:

内核工匠