内核同步

  • 内核服务请求的方式

    • 老板(硬件中断),客人(用户态发出的系统调用,或异常)
    • 不顾客人顾老板
    • 顾完新老板,再顾旧老板
    • 顾完老板,可能顾旧客人,也可能顾新客人
  • 抢占和并发

    • 抢占

      • 抢占式内核的特定:进程切换的时机多了。

        • 与抢占无关的切换:

          • 当前进程自愿放弃CPU(如:睡等资源)
          • 进程由内核态切回用户态时(一般是中断处理后,从中断返回的那一刻)。
          • 内核线程结束,或者调用调度函数的时候都会切换。
        • 抢占式内核特有的切换:
          • 在内核执行路径内部(甚至在执行一个内核函数过程中),也有可能被切换。

            • 例如:处理异常式,如果时间片用完,也会立即切换进程(非抢占式内核中,要等待异常处理完成。)
      • 优缺点:
        • 抢占的好处:减少被正运行的内核态进程延误的时间。
        • 正在执行不可重入的KCP时,不可抢占: 中断服务程序(但某些异常服务程序中,可以被抢占), softirq, tasklet
      • 禁用、使能、检查使能
        • 用pd.thread_info.preempt_count表示
        • 检查抢占使能:preempt_count
        • 抢占禁用(增加计数):get_cpu, preempt_disable
        • 抢占使能(减少计数): put_cpu(_noresched),preempt_enable(_noresched)
      • 抢占发生时机:
        • 中断/异常处理结束

          • 要恢复到用户空间前的进程切换:不是抢占特有的
          • 要恢复到内核空间前的进程切换:是抢占特有的
        • 抢占使能后,调用preempt_schedule函数:
          • 检查条件(preempt_count, 本地中断可用)
          • preempt_count 设置成“被抢占”
          • 调度
          • (再次获得CPU以后)preempt_count = 0 (恢复)
  • KCP并发

    • 造成KCP并发执行的三个原因:中断嵌套,抢占,多CPU同时访问,多个内核线程。各个因素会综合作用。
    • 并发影响的方面:中断处理、异常处理、可延时函数、内核线程。
    • 减少并发的设计方案。
      • 为了使中断处理不需同步:

        • 同类(同一根IRQ)中断不嵌套 (向PIC发ACK同时禁用此根IRQ线)
        • 中断处理, 可延迟函数(softirq、tasklet) 都要禁止抢占,并且要快速执行完(要求不能阻塞)
        • 硬件中断处理函数,不能被可延时函数或系统调用中断。
      • 为了使可延函不需同步:
        • 可延时函数不能并发。
        • 同一个可延时函数不能在多个CPU上同时执行。
        • 但不同可延函可以在不同CPU同时执行。

指令级的同步

  • per-CPU变量(实际是一个数组)

    • 原理:

      • 同一变量,每个CPU一份拷贝,只访问自己的,不访问别人的。
      • per-CPU 变量数组的元素要内存对齐,每个数据进入不同的cache块中; 以便能同时入cache, 而且同一个cache块没有不同CPU的数据.
    • 局限:
      • 能保证CPU,不能保证中断handle的同步。
      • 容易形成竞争条件。所以访问per-CPU变量时,不能抢占。
        • 如果获得本地per-CPU变量的地址,被抢占,然后迁移到其他CPU上执行。地址就是原来的CPU的数据地址,不是当前的
    • 相关接口:
      • 静态:

        • 定义per-CPU变量: DEFINE_PER_CPU(type, name),
        • 获取:per_cpu(name, cpu-id), 获取本地CPU变量并禁止抢占:get_cpu_var(name)
        • 释放:单纯使能抢占:put_cpu_var(name)
      • 动态:alloc_precpu (type), free_percpu (pointer) per_cpu_ptr(pointer, cpu-id)
  • 原子操作: 一条汇编指令,完成“读,修改,写回内存”,不能被中断。

    • 一般的“读改写”指令: (例如:inc dec)只适用于单CPU,不适用与多CPU

      • 内存不识别原子操作。例如:多CPU,如果同时执行“读,修改,写回内存”,内存操作会被内存仲裁成“读,读,写,写”。
    • 操作码有“lock”字节: 执行时锁住内存总线,其他CPU不能访问;适用于多CPU系统。
      • x86汇编,操作码加lock字节,执行该条指令时锁住内存。
      • linux中的c代码:
        • 原子计数器:类型是atomic_t;访问的函数、宏以atomic_开头。
        • 原子位操作(参数是地址):test_bit, set_bit, clear_bit, .....
  • 优化屏障和内存屏障

    • 优化屏障 - 编译器相关。防止屏障前后的指令,在优化时混合。用barrier()宏

      • 实现:asm volatile("":::"memory")

        • volatile关键字: 禁止编译器将内联汇编指令与其他指令调整。
        • memory关键字: 强制编译器认为内联汇编会改变RAM中所有的存储单元。所以编译器认为:asm以后的指令不能通过CPU寄存器,访问asm之前时的变量的值。所有不会优化。
      • 缺点:它不能保证CPU不会乱序执行。
    • 内存屏障 - CPU流水,乱序执行相关。前面的指令执行完毕后,再执行后面的指令。
      • 本身带有屏障作用(只能串行执行)的指令:

        • 操作I/O口的
        • 有lock字节的指令
        • 操作控制寄存器、系统寄存器、调试寄存器的的
        • 屏障(fence)指令:lfence(load), sfence(save), mfence(读写)
        • 一些特定的汇编指令,如iret
      • C代码中的宏:
        • 单CPU,多CPU共用: mb rmb wmb

          • CPU提供了屏障指令:内联屏障指令实现。
          • CPU没提供屏障指令:有优化屏障的,有lock字节的指令。
        • 仅多CPU用的:加smp_前缀.
          • 多CPU时:就是mb rmb wmb
          • 但CPU时:就是barrier宏

稍复杂的同步

  • 用自旋同步

    • 自旋锁

      • 特点:

        • 一般lock很短时间,多CPU时用
        • 自旋锁的临界区内,禁止抢占。否则会造成长时间占用锁。
        • 等待锁(自旋)时,可以抢占。
      • 数据结构:spinlock_t
        • slock: 锁的状态。1-打开状态; 0-锁上状态
        • break_lock: 是否有进程正在忙等该锁。(抢占式内核才会用到break_lock)
          • 长时间占据锁的进程,会查看break_lock,暂时放弃锁给忙等进程机会
          • 如果用spin_unlock + schedule + spin_lock, 会调用两次schedule函数; 所以提供了cond_resched_lock
            • 如果有忙等进程,解一下锁。(配置了SMP才会有该段代码)

              • 清break_lock, unlock, pause, lock
            • 如果需要调度,打开抢占,调度一下。
              • 开锁(_raw_spin_unlock),
              • 使能抢占(_no_resched),
              • 调度(__cond_resched),
              • 上锁。
      • spin_unlock: 简单给slock置1 (x86的写操作都是原子的),并使能抢占。
      • 抢占式内核的:spin_lock宏 (源代码:BUILD_LOCK_OPS)
        • 禁止抢占。
        • _raw_spin_trylock: 测试并设置”slock,如果返回-1,说明得到锁。
          • 用0来设置:如果原来是1,变成0;如果原来是0,结果仍是0。
        • 设置break_lock, 使能抢占。
        • 不断执行pause指令,直到锁被打开 或 break_lock变成0(只有cond_resched_lock会把break_lock清0)
        • 返回开始。
      • 非抢占式内核的spin_lock
        • slock原子减1,如果非负则得到锁
        • 如果小于0,pause等待,直到slock大于0 (被spin_unlock置1)
        • 返回开始。
    • 读写自旋锁 rwlock_t:结构与操作方式与spinlock类似,区别在于:
      • lock代替slock: 24bit(1-有写者;0-无写着位); 23-0bit(0x01000000减去读者数)

        • 这样lock的值从小到大,表示能进入的进程数 (读者占1,写者占极大值0x01000000)

          • 0:有写者, 一个也进不来
          • 中间:有读者,读者可进入
          • 0x01000000: 锁空闲
      • _raw_read_trylock, _raw_write_trylock 代替 _raw_spin_trylock。
        • 锁read时,lock减1;锁write时,lock减0x01000000。开锁时加上相应值
        • 锁read时,如果lock变成负数,则恢复0,忙等到1,再减1。
  • 利用重复(指令重复执行、数据多分拷贝)的同步

    • 指令重复执行:读时可写锁:seqlock (写者唯一)

      • 原理

        • spinlock_t: 写者互斥锁
        • sequence:写前加,写后再加1。偶数说明当前没写者;奇数说明当前有写者
        • 读前保存sequence(函数read_seqbegin), 读后比较(函数read_seqretry)。如果不一致就重读
      • 局限:
        • 不能出现:读者用指针引用数据,写者该指针。
        • 适用情况:读任务量小(临界区小),重读次数少(写不频繁)
    • 数据多分拷贝:RCU (读,拷贝更新)
      • 原理:

        • 数据通过指针引用。
        • 写时:拷贝数据,修改拷贝,新数据的指针替换原来数据的指针。
        • 旧数据,会在特定时刻,通过tasklet释放。
      • 接口:
        • 读数据:rcu_read_lock rcu_read_unlock 仅仅禁止、使能抢占。

          • 读数据的临界区不能有睡眠
          • kernel进入静止状态之前必须unlock。
            • 进程切换。
            • 返回到用户空间。
            • 开始idle。
        • 写数据完成后,调call_rcu(rcu_head). rcu_head一般内嵌在数据
          • call_rcu会把rcu_head插入一个链表中。
        • 每个tick, 会检查本地CPU是否进入静止状态。如果进入了,本地的rcu_tasklet会调用rcu_head内的函数释放数据。
          • rcu_tasklet是个per-CPU变量
  • 用等待列队同步

    • 信号量

      • 代码分析:

        • 阻塞时以独占方式插入列队,唤醒时只唤醒一个
        • 由于__up中的wake_up不操作列队; 所以多个__up时,wake_up的仅仅是第一个进程。
          • 这样__down中,本进程进入临界区前,再调一次wake_up_locked, 多唤醒一个进程(我们称为后继进程)。
          • 多唤醒的进程需要重试,才知道能不能真正唤醒。所以用for循环检查count,
          • 所以:睡眠时恢复原来的count. 唤醒后重新count--,测试count>=0;
        • 由于__up需要根据count来判断,是否有阻塞的进程
          • 所以,只有有睡眠的进程(无论多少),count就必须减一次(变成-1)。
          • 只要有__up, count就变成0了。第一个被唤醒的进程调wake_up_locked, 第二个进程就需要把count减1,以维持上述状态。
          • 所以就用sleepers, 表示有没有__up改变了上述状态。
      • 还原代码:
        • for 中的条件判断(原子完成:改值,判断非负):if (!atomic_add_negative(sem->sleepers-1, &sem->count))

          • 1、如果有__up, 被唤醒的进程要count-1, 否则count不变: count -= have_uped
          • 2、新进程来时:如果已经有等待的进程, count要恢复成down()以前的值; 否则不用恢复:count += sleepers
          • 结合以上两者:将have_uped替换成(1-sleepers).
            • 情况1变成:count += (sleepers - 1)
            • 为了满足情况2,新来的进程,进入for前,sleeper++;
            • 经过if判断后(无论是新来的,还是被唤醒的),根据情况设置sleeper: 0(不在“有睡眠的,count为-1”状态), 1(表示已进入上述状态)。
              • 当前进程被唤醒(或唤醒失败),就能推出有无__up.
      • spin_lock的作用:
        • 为了保护sem内数据。
        • down()之后,互斥锁没有锁上,所以也没有禁止抢占
    • 读写信号量
      • 严格的FIFO,每次都从列队头开始唤醒

        • 列队头是写锁:不再唤醒
        • 列队头是读锁:唤醒与之连续的所有读者,直到遇见一个写者。
      • 结构
        • count:

          • 高两字节:是否有写者进入 与 所有的等待数目
          • 低两字节:已进入的总数(无论读者或写者)
          • TODO: 具体编码形式:
        • wait_list: 等待列队,每一个元素都一个标识, 说明读者还是写者。
        • wait_lock: 互斥锁
      • 接口
        • down_read down_read_trylock up_read
        • down_write down_write_trylock up_write
        • down_grade_write: 把锁写直接变成锁读
    • 完成
      • 需求:A阻塞自己,等待B完成某些工作后唤醒自己。
      • 结构:completion:
        • uint done; 表示是否完成。正数:完成。负数:没有

          • 0:未完成,或已经跳过wait_for_completion
          • 1: 完成且还没有wait_for_completion.
        • wait_queue_head_t wait; 等待列队
      • 接口:
        • complete(): done++, 唤醒一个进程
        • wait_for_completion():
          • 如果done为0, 循环等待直到done非零,done减1。
          • 联系complete,可知done只能为0或1。不可能有其他值

系统级的同步

  • 本地中断禁用:维持本地CPU的KCP同步

    • 保护中断处理中的数据
    • 禁用、使能:local_irq_disable, local_irq_enble (cli, sli指令修改eflags的IF标识)
    • 禁用、恢复:local_irq_save, local_irq_restore
  • 延时函数禁用
    • 禁止中断后,可延迟函数没有了启动机会,自然就被禁止了
    • 单纯禁止可延迟函数:local_bh_disable: 修改preempt_count中的,softirq计数器部分。
    • 使能可延迟函数:local_bh_enable“:softirq计数器减1, 并使长时间等待的进程尽快执行
      • 检查是否能启动可延迟函数;如果能,调用soft_irq()启动
      • 检查TIF_NEED_RESCHED, 调preempt_schedule() 调度
  • 如何选取同步方式:高层并发,底层同步,且能少用就少用
    • 例如:链表操作:要插入的节点上先挂入后继节点上,再把前驱连上要插入的节点(需要加内存屏障)
    • 处理方式选择:考虑谁被谁打断,禁止前者
公用数据的指令 单一处理器 多处理器附加的机制
异常 信号量(产生同步问题的异常一般时系统调用。大多是资源相关,用等待列队方便。抢占不会产生问题,唯一需要禁止抢占的是访问per-CPU变量的时候。)
中断 中断禁用(只有中断禁用可以胜任) 自旋锁
可延时函数(softirqs) 无(单一CPU上,可延时函数只能串行执行。) 自旋锁(同一softirq可以同时在不同CPU上执行)
可延时函数(同一个tasklet) 无(同上) 无(因为同一个tasklet不能同时在不同CPU上执行,所以没有同步问题)
可延时函数(不同tasklet共用) 无(同上) 自旋锁(不用tasklet同一时间可在不同CPU上执行)
异常+中断处理 中断禁止(处理异常时可能来中断,但中断处理不可能被异常打断) 自旋锁(关注其他CPU上的中断和异常。快速处理完的中断,用自旋锁。系统调用的异常,用信号量好)
异常+可延迟函数 可延迟函数禁止(可延迟函数类似与异常,所以该处类似"异常+中断处理") 自旋锁
中断+可延迟函数 中断禁用(可延迟函数内可能有产生中断,反之不成立。所以,可延迟函数内禁止中断即可) 自旋锁
异常+中断+可延迟函数 中断禁用(异常处理、可延函都被中断打断,反之不成立) 自旋锁
  • 示例

    • 引用计数: 用原子类型(atomic_t)
    • 大内核锁:
      • 得不到就阻塞等待:用信号量实现
      • 得到锁的进程可以重复上锁;但上锁、开锁要匹配。
        • 用lock_depth表示锁的层次, 0表示要上锁;
        • 上锁时先加1,遇到0才真正上锁(down(sem))
        • 开锁时先减1, 小于0才真正开锁( up(sem) )
      • 自己调schedule时,要放弃锁;但被抢占时,不能放弃锁
        • schedule中:

          • if (prev_pd->lock_depth>=0) 释放锁(up信号量)
          • if (next_pd->lock_depth>=0) 申请锁(down信号量)
        • preempt_schedule_irq中:
          • 先把lock_depth置为-1,schedule()就不会释放锁了,其他的进程也就得不到大内核锁
          • schedule()返回后,再恢复原值

内核同步 (来自chinaunix总结)相关推荐

  1. Linux内核同步机制之(四):spin lock【转】

    转自:http://www.wowotech.net/kernel_synchronization/spinlock.html 一.前言 在linux kernel的实现中,经常会遇到这样的场景:共享 ...

  2. 内核同步机制-优化屏障和内存屏障

    优化屏障 编译器编译源代码时,会将源代码进行优化,将源代码的指令进行重排序,以适合于CPU的并行执行.然而,内核同步必须避免指令重新排序,优化屏障(Optimization barrier)避免编译器 ...

  3. linux 内核互斥体,Linux 内核同步(六):互斥体(mutex)

    互斥体 互斥体是一种睡眠锁,他是一种简单的睡眠锁,其行为和 count 为 1 的信号量类似.(关于信号量参考:Linux 内核同步(四):信号量 semaphore). 互斥体简洁高效,但是相比信号 ...

  4. 内核同步对性能的影响及perf的安装和简单的使用

    更多文章目录:点击这里 GitHub地址:https://github.com/ljrkernel 内核同步对性能的影响及perf的安装和简单的使用 看了一篇关于多线程应用程序性能分析的外文,结合之前 ...

  5. Linux内核之内核同步(一)——内核同步基础

    内核同步缘起何处? 提到内核同步,这还要从操作系统的发展说起.操作系统在进程未出现之前,只是单任务在单处理器cpu上运行,只是系统资源利用率低,并不存在进程同步的问题.后来,随着操作系统的发展,多进程 ...

  6. 第4章 第三节 内核同步

    抢占式内核和非抢占式内核 Linux 内核有两个空间,一个是内核空间一个是用户空间,如果一个进程正在内核态执行的时候,允许内核打断他的执行,让另一个进程执行,那么这个内核就是可抢占式内核. 还有一种情 ...

  7. linux 内核同步--理解原子操作、自旋锁、信号量(可睡眠)、读写锁、RCU锁、PER_CPU变量、内存屏障

    内核同步 内核中可能造成并发的原因: 中断–中断几乎可以在任何时刻异步发生,也就可以随时打断当前正在执行的代码. 软中断和tasklet–内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在 ...

  8. Linux内核同步机制之信号量与锁

    Linux内核同步控制方法有很多,信号量.锁.原子量.RCU等等,不同的实现方法应用于不同的环境来提高操作系统效率.首先,看看我们最熟悉的两种机制--信号量.锁. 一.信号量 首先还是看看内核中是怎么 ...

  9. linux 多进程 同步,Linux内核同步,进程,线程同步

    包括我自己在内,很多人对内核,进程,线程同步都不是很清楚,下面稍微总结一下: 内核同步: 主要是防止多核处理器同时访问修改某段代码,或者在对设备驱动程序进行临界区保护.主要有一下几种方式: 1. Mu ...

最新文章

  1. visual2017中给C#项目添加配置文件
  2. 程序员因拒绝带电脑回家被开除,获赔 19.4 万元
  3. 《中国人工智能学会通讯》——4.14 相关研究现状
  4. TCP协议经典书籍--TCP/IP详解
  5. canopen服务器协议,CANopen
  6. linux 下 select 函数的用法
  7. 平台电商类的增长策略:从用户激励到养成类游戏
  8. 代码中特殊的注释技术——TODO、FIXME和XXX的用处
  9. ubuntu 10.04下vmware tools安装和一些应用
  10. PDF Converter 注册码
  11. linux卸载windows boot,windows和Linux双系统卸载Linux系统
  12. 苏州大学在职研究生计算机专业,苏州大学在职研究生有哪些专业?
  13. android图标重力感应插件,重力感应,图片摆动旋转(自定义控件) android
  14. JavaScript 练手小技巧:过年了,用JS写一幅春联吧
  15. R语言实战 第2版 中文目录
  16. 无主键mysql表创建主键
  17. java工单系统源码_基于jsp的工单管理系统-JavaEE实现工单管理系统 - java项目源码...
  18. Android音视频【三】硬解码播放H264
  19. STM32F4系列单片机选型详解
  20. 【出差总结】出差0902

热门文章

  1. linux中pss用法,[Linux] Memory: VSS/RSS/PSS/USS
  2. 时间序列趋势判断(二)——Cox-Staut趋势检验
  3. 解决pytouch导入模型报错:AttributeError: Can‘t get attribute ‘XXX‘ on <module ‘__main__‘ from XXX>
  4. java appium_Android应用开发之AS+Appium+Java+Win自动化测试之Appium的Java测试脚本封装(Android测试)...
  5. 【题解】BZOJ5093图的价值(二项式+NTT)
  6. 使用C# impersonation进行windows帐号的校验
  7. 深度学习之正则化方法
  8. Andrew Ng机器学习编程作业:K-means Clustering and Principal Component Analysis
  9. chattr 改变文件的扩展属性
  10. HTML5为输入框添加语音输入功能