内核同步 (来自chinaunix总结)
内核同步
- 内核服务请求的方式
- 老板(硬件中断),客人(用户态发出的系统调用,或异常)
- 不顾客人顾老板
- 顾完新老板,再顾旧老板
- 顾完老板,可能顾旧客人,也可能顾新客人
- 抢占和并发
- 抢占
- 抢占式内核的特定:进程切换的时机多了。
- 与抢占无关的切换:
- 当前进程自愿放弃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, .....
- 一般的“读改写”指令: (例如:inc dec)只适用于单CPU,不适用与多CPU
- 优化屏障和内存屏障
- 优化屏障 - 编译器相关。防止屏障前后的指令,在优化时混合。用barrier()宏
- 实现:asm volatile("":::"memory")
- volatile关键字: 禁止编译器将内联汇编指令与其他指令调整。
- memory关键字: 强制编译器认为内联汇编会改变RAM中所有的存储单元。所以编译器认为:asm以后的指令不能通过CPU寄存器,访问asm之前时的变量的值。所有不会优化。
- 缺点:它不能保证CPU不会乱序执行。
- 实现:asm volatile("":::"memory")
- 内存屏障 - 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宏
- 单CPU,多CPU共用: mb rmb wmb
- 本身带有屏障作用(只能串行执行)的指令:
- 优化屏障 - 编译器相关。防止屏障前后的指令,在优化时混合。用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),
- 上锁。
- 如果有忙等进程,解一下锁。(配置了SMP才会有该段代码)
- 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: 锁空闲
- 这样lock的值从小到大,表示能进入的进程数 (读者占1,写者占极大值0x01000000)
- _raw_read_trylock, _raw_write_trylock 代替 _raw_spin_trylock。
- 锁read时,lock减1;锁write时,lock减0x01000000。开锁时加上相应值
- 锁read时,如果lock变成负数,则恢复0,忙等到1,再减1。
- lock代替slock: 24bit(1-有写者;0-无写着位); 23-0bit(0x01000000减去读者数)
- 自旋锁
- 利用重复(指令重复执行、数据多分拷贝)的同步
- 指令重复执行:读时可写锁: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变量
- 读数据:rcu_read_lock rcu_read_unlock 仅仅禁止、使能抢占。
- 原理:
- 指令重复执行:读时可写锁:seqlock (写者唯一)
- 用等待列队同步
- 信号量
- 代码分析:
- 阻塞时以独占方式插入列队,唤醒时只唤醒一个
- 由于__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.
- for 中的条件判断(原子完成:改值,判断非负):if (!atomic_add_negative(sem->sleepers-1, &sem->count))
- spin_lock的作用:
- 为了保护sem内数据。
- down()之后,互斥锁没有锁上,所以也没有禁止抢占
- 代码分析:
- 读写信号量
- 严格的FIFO,每次都从列队头开始唤醒
- 列队头是写锁:不再唤醒
- 列队头是读锁:唤醒与之连续的所有读者,直到遇见一个写者。
- 结构
- count:
- 高两字节:是否有写者进入 与 所有的等待数目
- 低两字节:已进入的总数(无论读者或写者)
- TODO: 具体编码形式:
- wait_list: 等待列队,每一个元素都一个标识, 说明读者还是写者。
- wait_lock: 互斥锁
- count:
- 接口
- down_read down_read_trylock up_read
- down_write down_write_trylock up_write
- down_grade_write: 把锁写直接变成锁读
- 严格的FIFO,每次都从列队头开始唤醒
- 完成
- 需求:A阻塞自己,等待B完成某些工作后唤醒自己。
- 结构:completion:
- uint done; 表示是否完成。正数:完成。负数:没有
- 0:未完成,或已经跳过wait_for_completion
- 1: 完成且还没有wait_for_completion.
- wait_queue_head_t wait; 等待列队
- uint done; 表示是否完成。正数:完成。负数:没有
- 接口:
- 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()返回后,再恢复原值
- schedule中:
内核同步 (来自chinaunix总结)相关推荐
- Linux内核同步机制之(四):spin lock【转】
转自:http://www.wowotech.net/kernel_synchronization/spinlock.html 一.前言 在linux kernel的实现中,经常会遇到这样的场景:共享 ...
- 内核同步机制-优化屏障和内存屏障
优化屏障 编译器编译源代码时,会将源代码进行优化,将源代码的指令进行重排序,以适合于CPU的并行执行.然而,内核同步必须避免指令重新排序,优化屏障(Optimization barrier)避免编译器 ...
- linux 内核互斥体,Linux 内核同步(六):互斥体(mutex)
互斥体 互斥体是一种睡眠锁,他是一种简单的睡眠锁,其行为和 count 为 1 的信号量类似.(关于信号量参考:Linux 内核同步(四):信号量 semaphore). 互斥体简洁高效,但是相比信号 ...
- 内核同步对性能的影响及perf的安装和简单的使用
更多文章目录:点击这里 GitHub地址:https://github.com/ljrkernel 内核同步对性能的影响及perf的安装和简单的使用 看了一篇关于多线程应用程序性能分析的外文,结合之前 ...
- Linux内核之内核同步(一)——内核同步基础
内核同步缘起何处? 提到内核同步,这还要从操作系统的发展说起.操作系统在进程未出现之前,只是单任务在单处理器cpu上运行,只是系统资源利用率低,并不存在进程同步的问题.后来,随着操作系统的发展,多进程 ...
- 第4章 第三节 内核同步
抢占式内核和非抢占式内核 Linux 内核有两个空间,一个是内核空间一个是用户空间,如果一个进程正在内核态执行的时候,允许内核打断他的执行,让另一个进程执行,那么这个内核就是可抢占式内核. 还有一种情 ...
- linux 内核同步--理解原子操作、自旋锁、信号量(可睡眠)、读写锁、RCU锁、PER_CPU变量、内存屏障
内核同步 内核中可能造成并发的原因: 中断–中断几乎可以在任何时刻异步发生,也就可以随时打断当前正在执行的代码. 软中断和tasklet–内核能在任何时刻唤醒或调度软中断和tasklet,打断当前正在 ...
- Linux内核同步机制之信号量与锁
Linux内核同步控制方法有很多,信号量.锁.原子量.RCU等等,不同的实现方法应用于不同的环境来提高操作系统效率.首先,看看我们最熟悉的两种机制--信号量.锁. 一.信号量 首先还是看看内核中是怎么 ...
- linux 多进程 同步,Linux内核同步,进程,线程同步
包括我自己在内,很多人对内核,进程,线程同步都不是很清楚,下面稍微总结一下: 内核同步: 主要是防止多核处理器同时访问修改某段代码,或者在对设备驱动程序进行临界区保护.主要有一下几种方式: 1. Mu ...
最新文章
- visual2017中给C#项目添加配置文件
- 程序员因拒绝带电脑回家被开除,获赔 19.4 万元
- 《中国人工智能学会通讯》——4.14 相关研究现状
- TCP协议经典书籍--TCP/IP详解
- canopen服务器协议,CANopen
- linux 下 select 函数的用法
- 平台电商类的增长策略:从用户激励到养成类游戏
- 代码中特殊的注释技术——TODO、FIXME和XXX的用处
- ubuntu 10.04下vmware tools安装和一些应用
- PDF Converter 注册码
- linux卸载windows boot,windows和Linux双系统卸载Linux系统
- 苏州大学在职研究生计算机专业,苏州大学在职研究生有哪些专业?
- android图标重力感应插件,重力感应,图片摆动旋转(自定义控件) android
- JavaScript 练手小技巧:过年了,用JS写一幅春联吧
- R语言实战 第2版 中文目录
- 无主键mysql表创建主键
- java工单系统源码_基于jsp的工单管理系统-JavaEE实现工单管理系统 - java项目源码...
- Android音视频【三】硬解码播放H264
- STM32F4系列单片机选型详解
- 【出差总结】出差0902
热门文章
- linux中pss用法,[Linux] Memory: VSS/RSS/PSS/USS
- 时间序列趋势判断(二)——Cox-Staut趋势检验
- 解决pytouch导入模型报错:AttributeError: Can‘t get attribute ‘XXX‘ on <module ‘__main__‘ from XXX>
- java appium_Android应用开发之AS+Appium+Java+Win自动化测试之Appium的Java测试脚本封装(Android测试)...
- 【题解】BZOJ5093图的价值(二项式+NTT)
- 使用C# impersonation进行windows帐号的校验
- 深度学习之正则化方法
- Andrew Ng机器学习编程作业:K-means Clustering and Principal Component Analysis
- chattr 改变文件的扩展属性
- HTML5为输入框添加语音输入功能