oxlm_1

  • 2024-05-27
  • 回复了主题帖: AI语音模块咋用

    我觉着吧,你这场景,更大的问题是降噪,语音识别反而变成次要的了。

  • 2024-04-18
  • 回复了主题帖: 阅读打卡终点站:安全漏洞分析——《奔跑吧Linux内核2:调试与案例分析》

    1.请简述高速侧信道攻击的原理。 由于CPU的处理速度远大于内物理内存的访问速度,因此CPU设计了多级缓存结构,如果内存数据被放置在告诉缓存中,则CPU会在较短时间内访问内存,否则,则直接从物理内存中读取数据,这个内存的访问时间相对来说比较长。攻击者会利用这个时间差来进行攻击。 2.在CPU熔断漏洞攻击中,攻击者在用户态访问内核空间时会发生异常,攻击者进程会被终止,这样导致后续无法进行侧信道攻击,那么如何解决这个问题? 在攻击者进程中设置异常处理信号。当发生异常时调用该型号的回调函数,从而抑制异常导致的攻击过程的失败。 3.请简述熔断漏洞攻击的原理和过程。 熔断漏洞攻击原理:巧妙地利用了现代处理器中乱序执行的副作用进行侧信号攻击,破坏了基于地址空间隔离的安全机制,使得用户态程序可以读出内核空间的数据。 过程: 1. 注册异常处理函数(侧信号攻击) 2. 从TLB中获取内核页表中的物理地址 3. 利用MMU查询页表以及通过MMU访问内核地址产生异常消息 4. 在异常处理函数中分析消息 当有攻击者想在用户态访问内核地址空间时,CPU会查询TLB。由于此时内核页表的TLB是全局类型的,因此可能从TLB中查到物理地址。另外,CPU通过MMU去查询页表。CPU访问内核空间的地址最终会产生异常,但因为乱序执行,CPU会提前预取了内核空间的数据,这就导致了熔断漏洞。

  • 2024-04-09
  • 回复了主题帖: 阅读打卡第五站:基于ARM64解决宕机难题——《奔跑吧Linux内核2:调试与案例分析》

    1.假设函数调用关系为main()->func1()->func2(),请画出ARM64架构的函数栈的布局。   2.在ARM64架构中,子函数的栈空间的FP指向哪里? 指向父函数的FP 3.在ARM64架构的calltrace日志里,如何推导出函数的名称? 使用dis命令获取对应的函数名称以及在该函数中执行的位置

  • 2024-04-02
  • 回复了主题帖: 《奔跑吧Linux内核(第2版)卷2:调试与案例分析》- 中断管理

    hellokitty_bean 发表于 2024-4-2 09:02 谢谢楼主回复。。。。 貌似没有试读,只能自己找找来学习学习 如今迈步从头“阅”了 其实买一本也不错,老实说,个人觉得看纸质书还是比看电子书效率高,至少在书本上做笔记的便捷程度比电脑操作好太多了。

  • 2024-04-01
  • 回复了主题帖: 阅读打卡第四站:基于x86_64解决宕机难题——《奔跑吧Linux内核2:调试与案例分析》

    1.请简述Kdump的工作原理。 基于Kexec,在系统崩溃时快速切换到一个备份的内核,保留第一个内核的内存信息,同时第二个内核对第一个内核的内存数据进行分析并将数据和分析结果保存下来。 2.在x84_64架构里函数参数是如何传递的? 当函数中参数的数量少于或等于6个时,使用通用寄存器来传递函数的参数。当函数的参数的数量大于6时,采用栈空间来传递函数的参数。 3.假设函数调用关系为main()→func1()→func2(),请画出x84_64架构的函数栈的布局图。  

  • 2024-03-31
  • 发表了主题帖: 《奔跑吧Linux内核(第2版)卷2:调试与案例分析》- 中断管理

           看完第一章并发与同步后,接下来便是 中断管理,这一章前面部分从硬件层面的中断出发逻辑到系统处理中断一路讲解下来。直到软中断和tasklet中才真正把中断处理讲解完毕。另外,补充了一个知识点,为解决中断的优先级过高导致多软中断处理时导致的系统调度受阻问题,引入了workqueue工作队列相关的知识。         中断触发过程,从出发过程中看,其实arm64的硬件中断触发逻辑与cortex-m核的中断管理大致类似,都是各种中断源触发中断信号,处理器接收到中断信号后,通过查询寄存器判定是什么中断触发了,之后做一系列的中断处理。唯一有区别的是,arm64中断存在多核的场景(m核也是在近几年才出现多核异构的场景,但实际使用上,与linux的多核任务处理还是有明显差异),中断消息传递逻辑会更加复杂一些,另外中断源数相比于m核来说,完全不是一个数量级,因此存在多级中断的概念,以降低中断源判断的复杂度。        之后是从arm64的角度分析linux内核中断处理过程,目前从应用角度上的总结信息如下: 中断注册: request_irq() // 比较旧的接口函数 request_thread_irq() // 2.6.30内核新增的线程化中断注册函数 // 注意: // 使用IRQ号(映射过的软件中断号),而不是硬件中断号。 // 主处理程序和thread_fn不能同时为NULL // 当主处理程序为NULL且硬件中断控制器不支持硬件ONESHOT功能时,应该显式地设置IRQ_ONESHOT标志来确保不会产生中断风暴 // 若启用了中断线程化,那么primary handler应该返回IRQ_WAKE_THREAD来唤醒中断线程 setup_irq_thread() // 创建一个实时线程,调度策略为SCHED_FIFO,优先级50 Linux中断调用流程: //ARM64 Linux硬件中断注册流程 __git_init_bases() set_handle_irq(gic_handle_irq)// 使handle_arch_irq 指向 gic_handle_irq() //ARM64 Linux中断流程 kernel_entery // 保存中断上下文 irq_handler //中断处理 irq_stack_entry // 设置栈地址 handle_arch_irq = gic_handle_irq() handle_domain_irq() //Linux内核中断处理入口 irq_enter() //显式地告知内核现在进入中断上下文 preempt_count_add(HARDIRQ_OFFSET) // 增加当前进程的thread_infod中preempt_count成员里HARDIRQ域的值 irq_find_mapping() //通过硬件中断号找到IRQ号 generic_handle_irq() handle_fasteoi_irq() // 如果没有指定action描述符或者该中断关闭了IRQD_IRQ_DISABLED,则设置中断状态为IRQ_PENDING, // 然后调用中断控制器的irq_chip中的irq_mask()回调函数屏蔽该终端 // 如果该中断不支持中断嵌套,则调用mask_irq()函数关闭该中断源 handle_irq_event() // 中断处理的核心函数 handle_irq_event_percpu() // 对应cpu的中断处理入口 __handle_irq_event_percpu() // 同上 action->handler() // 执行中断处理,若需要唤醒中断的内核线程,则返回IRQ_WAKE_THREAD __irq_wake_thread() //若需要,则唤醒中断的内核线程 irq_exit() //显式地告知内核现在退出中断上下文 preempt_count_sub(HARDIRQ_OFFSET) //配对地递减preempt_count中HARDIRQ域地计数 invoke_softirq() __do_softirq() // 处理软中断 irq_stack_exit // 从中断栈切换回中断进程地内核栈 kernel_exit // 恢复中断上下文并退出中断 // __irq_wake_thread()唤醒中断线程后的执行过程 irq_thread() //中断线程的执行函数,设置handler_fn()回调 irq_thread_fn() // 执行注册中断时的thread_fn()函数 atcion->thread_fn() //最终的中断处理入口 irq_finalize_oneshot() //对IRQS_ONESHOT类型中断的处理 Linux软中断:     由中断处理中可以发现,Linux的软中断处理实际上也是在中断调用流程中实现的。 open_softirq(int nr, void(*action)(struct softirq_action *)) //注册一个软中断,其中nr是软中断的序号 //触发软中断接口 #ifndef local_softirq_pending_ref #define local_softirq_pending_ref irq_stat.__softirq_pending #endif #define local_softirq_pending() (__this_cpu_read(local_softirq_pending_ref)) #define set_softirq_pending(x) (__this_cpu_write(local_softirq_pending_ref, (x))) #define or_softirq_pending(x) (__this_cpu_or(local_softirq_pending_ref, (x))) // 设置irq_stat数据结构中__soft_pending成员的第nr位,nr表示中断序号。 // 在中断返回时,该CPU会检查此信息,若不为0,则说明有pending的软中断需处理 void __raise_softirq_irqoff(unsigned int nr) { trace_softirq_raise(nr); or_softirq_pending(1UL << nr); } inline void raise_softirq_irqoff(unsigned int nr) // 主动关闭本地中断,允许在进程上下文中调用 { __raise_softirq_irqoff(nr); if (!in_interrupt()) wakeup_softirqd(); } void raise_softirq(unsigned int nr) { unsigned long flags; local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags); } Tasklet Tasklet本质上是软中断的一个实现形式,运行在软中断的上下文中 //驱动中tasklet使用 // 定义 // 静态声明 #define DECLARE_TASKLET(name, func, data) \ struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data } // 初始化并默认使能 #define DECLARE_TASKLET_DISABLED(name, func, data) \ struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } // 初始化并默认禁用 //动态声明 void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data) { t->next = NULL; t->state = 0; atomic_set(&t->count, 0); t->func = func; t->data = data; } // 使用Tasklet //在驱动中调用tasklet_schedule()函数 // Tasklet执行,遵循软中断上下文逻辑,因此不是在调用__tasklet_schedule_common时就立即执行,而是需要等待调度时执行 static void __tasklet_schedule_common(struct tasklet_struct *t, struct tasklet_head __percpu *headp, unsigned int softirq_nr) { struct tasklet_head *head; unsigned long flags; local_irq_save(flags); head = this_cpu_ptr(headp); t->next = NULL; *head->tail = t; head->tail = &(t->next); raise_softirq_irqoff(softirq_nr); local_irq_restore(flags); } void __tasklet_schedule(struct tasklet_struct *t) { __tasklet_schedule_common(t, &tasklet_vec, TASKLET_SOFTIRQ); } static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t); }        中断管理看完后,下一节的内容便是着重介绍工作队列(workqueue)部分。workqueue的引入是为了解决软中断和tasklet的天然硬伤,由于其处理优先级比系统调度要高,因此在软中断较多的场景下,系统调度的及时性会大大降低。而工作队列将任务交由特定线程完成,也就是说,工作队列的任务处理是在系统调度中实现,也就规避了系统调度被长时间阻塞的问题。         工作队列核心代码的实现如下: //工作队列初始化, 系统初始化时自动运行, 驱动开发者不需要操作 // 图像化的流程见附图2.10 start_kernel() workqueue_init_early() // 用于初始化默认的几个工作队列 arch_call_rest_init() rest_init() kernel_thread(kernel_init, NULL, CLONE_FS); kernel_init() kernel_init_freeable() workqueue_init() create_worker() // 创建工作队列 worker_attach_to_pool() // 把刚分配的工作线程挂入worker_poolworker_pool中,并且设置这个工作线程允许运行的cpumask // 创建工作队列 // 其中 __alloc_workqueue_key流程见附图2.11 #ifdef CONFIG_LOCKDEP #define alloc_workqueue(fmt, flags, max_active, args...) \ ({ \ static struct lock_class_key __key; \ const char *__lock_name; \ \ __lock_name = "(wq_completion)"#fmt#args; \ \ __alloc_workqueue_key((fmt), (flags), (max_active), \ &__key, __lock_name, ##args); \ }) #else #define alloc_workqueue(fmt, flags, max_active, args...) \ __alloc_workqueue_key((fmt), (flags), (max_active), \ NULL, NULL, ##args) #endif #define alloc_ordered_workqueue(fmt, flags, args...) \ alloc_workqueue(fmt, WQ_UNBOUND | __WQ_ORDERED | \ __WQ_ORDERED_EXPLICIT | (flags), 1, ##args) #define create_workqueue(name) \ alloc_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, 1, (name)) #define create_freezable_workqueue(name) \ alloc_workqueue("%s", __WQ_LEGACY | WQ_FREEZABLE | WQ_UNBOUND | \ WQ_MEM_RECLAIM, 1, (name)) #define create_singlethread_workqueue(name) \ alloc_ordered_workqueue("%s", __WQ_LEGACY | WQ_MEM_RECLAIM, name) //添加工作队列 #define ATOMIC_LONG_INIT(i) ATOMIC_INIT(i) #define WORK_DATA_INIT() ATOMIC_LONG_INIT((unsigned long)WORK_STRUCT_NO_POOL) #ifdef CONFIG_LOCKDEP #define __INIT_WORK(_work, _func, _onstack) \ do { \ static struct lock_class_key __key; \ \ __init_work((_work), _onstack); \ (_work)->data = (atomic_long_t) WORK_DATA_INIT(); \ lockdep_init_map(&(_work)->lockdep_map, "(work_completion)"#_work, &__key, 0); \ INIT_LIST_HEAD(&(_work)->entry); \ (_work)->func = (_func); \ } while (0) #else #define __INIT_WORK(_work, _func, _onstack) \ do { \ __init_work((_work), _onstack); \ (_work)->data = (atomic_long_t) WORK_DATA_INIT(); \ INIT_LIST_HEAD(&(_work)->entry); \ (_work)->func = (_func); \ } while (0) #endif #define INIT_WORK(_work, _func) \ __INIT_WORK((_work), (_func), 0) #define INIT_WORK_ONSTACK(_work, _func) \ __INIT_WORK((_work), (_func), 1) //调度工作队列 //__queue_work()为调度工作队列的核心实现,其大致实现流程见图2.12 //需注意,调度工作队列,实际上并未执行工作队列,工作队列的具体执行见工作队列初始化中的create_worker()函数中调用的worker_thread()函数 //简化后的worker_thread()函数实现如下: worker_thread() { recheck: if(不需要更多的工作线程?) goto sleep; if(需要创建更多的工作线程? && 创建工作线程) goto recheck; do { 处理工作; }(还有工作未完成 && 活跃的工作线程 <= 1) sleep: schedule(); } bool queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work) { bool ret = false; unsigned long flags; local_irq_save(flags); if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT, work_data_bits(work))) { __queue_work(cpu, wq, work); ret = true; } local_irq_restore(flags); return ret; } static inline bool queue_work(struct workqueue_struct *wq, struct work_struct *work) { return queue_work_on(WORK_CPU_UNBOUND, wq, work); } static inline bool schedule_work(struct work_struct *work) { return queue_work(system_wq, work); } //取消一个workqueue cancel_work_sync() // 取消一个work,但会等到work执行完毕 __cancel_work_timer() try_to_grab_pending() // 尝试从线程池中拿到所需要取消的work,若未拿到,则继续执行__cancel_work_timer() __flush_work() // 等待work执行完毕 总结        1. 或许是之前并未深入研究linux内核源码的缘故,阅读第二章的知识内容,仅仅从怎么使用的角度去理解,都有一种较为吃力的感觉,此感觉在workqueue部分特别明显,明显感觉需要反复对照源码才能理解逻辑。因此此章节的知识点还需要反复阅读学习,并配以项目实践来纠正和加深理解。 2. 在反复看前两章内容的过程中,明显感觉缺乏卷1内容的情况下直接看卷2,有不少知识点盲区。因此看此书时最好备有卷1,以便出现涉及到卷1内容时快速查阅。 3. CMWQ机制目前还没理解透, 还需继续深入阅读理解。

  • 发表了日志: 《奔跑吧Linux内核(第2版)卷2:调试与案例分析》- 中断管理

  • 2024-03-25
  • 回复了主题帖: 阅读打卡第三站: 内核调试与性能优化——《奔跑吧Linux内核2:调试与案例分析》

    1.使用GCC的O0优化选项来编译内核有什么优势? O0表示编译时不做优化,正因为不会优化,因此在线debug时不会出现指针乱跳的情况,可以精准定位到问题点,帮助分析问题。缺点也很直接,未经过优化的代码,所占用的空间会变大,且运行效率大概率不是最优的。 2.什么是加载地址、运行地址和链接地址? 加载地址:存储代码的物理地址 运行地址:程序运行时的地址 链接地址:在编译链接时指定的地址,编程人员设想将来程序要运行的地址 3.什么是位置无关的汇编指令?什么是位置有关的汇编指令? 位置无关的汇编指令,即这部分汇编指令的执行与内存地址无关;无论链接地址和链接地址是否相等,这部分指令都能正常运行。 位置有关的汇编指令,即这部分汇编指令的执行是与内存地址有关的,和当前PC值无关。

  • 2024-03-22
  • 回复了主题帖: 阅读打卡第二站:中断管理——《奔跑吧Linux内核2:调试与案例分析》

    发生硬件中断后,ARM64处理器做了哪些事情? 将中断标记为pending状态 对于处于pending状态的中断,仲裁单元hui'que'dinghuiqueding 目标CPU,将中断请求发送到这个CPU 对于每个CPU,仲裁单元会从众多处于pending状态的中断中选择一个优先级最高的中断,发送到目标CPU的CPU接口模块上 CPU接口模块会决定这个中断是否可以发给CPU。如果该中断的优先级满足要求,GIC会发送一个中断请求信号给该CPU 当一个CPU进入中断异常后,会读取GICC_IAR来响应该中断。寄存器会返回硬件中断号,对于SGI来说,返回源CPU的ID。当GIC感知到软件读取了该寄存器后,又分为如下情况: 如果该中断处于pending状态,则将状态置为active 如果该中断又重新产生,那么将pending状态变为active and pending状态 如果该中断处于active状态,则变成active and pending状态 当处理器完成中断服务,必须发送一个完成信号结束中断给GIC 2.硬件中断号和Linux内核的IRQ号是如何映射的? 使用irq_domain的管理框架来管理中断,具体模式如下: 3.一个硬件中断发生后,Linux内核如何响应并处理该中断? handle_domain_irq() //Linux内核中断处理入口 irq_enter() //显式地告知内核现在进入中断上下文 irq_find_mapping() //通过硬件中断号找到IRQ号 generic_handle_irq() handle_fasteoi_irq() // 如果没有指定action描述符或者该中断关闭了IRQD_IRQ_DISABLED,则设置中断状态为IRQ_PENDING, // 然后调用中断控制器的irq_chip中的irq_mask()回调函数屏蔽该终端 // 如果该中断不支持中断嵌套,则调用mask_irq()函数关闭该中断源 handle_irq_event() // 中断处理的核心函数 handle_irq_event_percpu() // 对应cpu的中断处理入口 __handle_irq_event_percpu() // 同上 action->handler() // 执行中断处理,若需要唤醒中断的内核线程,则返回IRQ_WAKE_THREAD __irq_wake_thread() //若需要,则唤醒中断的内核线程 irq_exit() //显式地告知内核现在退出中断上下文 // __irq_wake_thread()唤醒中断线程后的执行过程 irq_thread() //中断线程的执行函数,设置handler_fn()回调 irq_thread_fn() // 执行注册中断时的thread_fn()函数 atcion->thread_fn() //最终的中断处理入口 irq_finalize_oneshot() //对IRQS_ONESHOT类型中断的处理  

  • 回复了主题帖: 国产芯榜:辛昕推荐新发现的1.8元蓝牙芯片,伦茨ST17H66,大家怎么看?

    蓝牙其实最难的是兼容性和技术支持,兼容性做不好,技术支持也跟不上,价格再便宜也不敢用。不知道这家这两块做的咋样

  • 2024-03-20
  • 发表了日志: 《奔跑吧Linux内核(第2版)卷2:调试与案例分析》- 并发与同步 知识点总结

  • 发表了主题帖: 《奔跑吧Linux内核(第2版)卷2:调试与案例分析》- 并发与同步 知识点总结

    本帖最后由 oxlm_1 于 2024-3-20 15:21 编辑 前言         本来想看完第一章后,以RTOS和Linux互斥锁机制之间的差异的角度来解析为何两种系统存在差异,RTOS是否有必要实现Linux这类复杂的锁机制。但阅读完后发现Linux锁机制因为迭代版本多,内容较为复杂,前置知识较多,因此改为写第一章使用角度的知识点总结。 原子操作 原子变量定义 <include/linux/types.h> typedef struct { int counter; }atomic_t;         atomic_t类型的原子操作函数可以保证一个操作的原子性和完整性。而要原子的保证操作的完整性和原子性,通常需要“原子地”(不间断地)完成“读-修改-回写”机制,中间不能被打断。如果其他CPU同时对该原子变量进行操作,则会影响数据完整性。 原子操作函数         处理器必须提供原子操作地汇编指令来完成原子操作,如arm64处理器地cas,x86的cmpxchg指令等。 <include/asm-generic/atomic.h> #define ATOMIC_INIT(i) //将原子变量初始化为i #define atomic_read(v) //读取原子变量的值 #define atomic_set(v, i) //设置原子变量v的值为i // 不带返回值的原子操作 atomic_inc(v) // 原子地增1 atomic_dev(v) // 原子地减1 atomic_add(i, v)// 原子地给v加i atomic_and(i, v) // 原子地给v和i做“与”操作 atomic_or(i, v)// 原子地给v和i做“或”操作 atomic_xor(i, v)// 原子地给v和i做“异或”操作 // 带返回值的原子操作 // 返回值为新值的原子操作 atomic_add_return(int i, atomic_t *v) // 原子地给v加i并返回v的新值 atomic_sub_return(int i,atomic_t *v) // 原子地给v减i并返回v的新值 atomic_inc_return(v) // 原子地给v加1并返回v的新值 atomic_dec_return(v) // 原子地给v减1并返回v的新值 //返回值为旧值的原子操作 atomic_fetch_add(int i, atomic_t *v) // 原子地给v加i并返回v的旧值 atomic_fetch_sub(int i,atomic_t *v) // 原子地给v减i并返回v的旧值 atomic_fetch_and(int i,atomic_t *v) // 原子地给v和i做与操作并返回v的旧值 atomic_fetch_or(int i,atomic_t *v) // 原子地给v和i做或操作并返回v的旧值 atomic_fetch_xor(int i,atomic_t *v) // 原子地给v和i做异或操作并返回v的旧值 //原子交换函数 atomic_cmpxchg(ptr, old, new) // 原子地比较ptr的值是否与old的值相等,若相等,则把new的值设置到ptr地址中,返回old的值 atomic_xchg(ptr, new) // 原子地把new的值设置到ptr地址中并返回ptr的原值 atomic_try_cmpxchg(ptr, old, new) // 与atomic_cmpxchg()函数类似,但返回值发生变化,防火一个bool值,用于判断cmpxchg()函数的返回值是否与old的值相等 //内嵌内存屏障原语的原子操作函数 // {}代表前面出现的函数名 // 内存屏障相关知识,在《卷1》中有介绍 {}_relaxed // 不内嵌内存屏障原语 {}_anquire // 内置了加载-获取内存屏障原语 {}_release // 内置了存储-释放内存屏障原语   自旋锁 自旋锁使用前提:1. 临界区不允许发生抢占 自旋锁的作用:实现一个忙时等待的锁 在中断上下文中可以毫不犹豫的使用自旋锁,如果临界区有睡眠、隐含睡眠的动作及内核接口函数,应避免选择自旋锁。 经典自旋锁 最初版本的自旋锁 由于只有一个锁标记表示锁是否被持有,在锁争用的情况下可能会导致高优先级的任务一直获取锁,而低优先级的任务长时间获取不到锁。 基于排队的自旋锁 为了解决最初版本自旋锁的问题,将32字节的锁标记拆分为owner标记和next标记,owner标记表征当前自旋锁执行到的位置,而next自旋锁表示目前排队的任务所拿到的最大编号。当owner执行完毕后,owner加1,此时将锁传递为持有新owner值的任务。 MCS锁 由于基于排队的自旋锁没法解决高速缓存行颠簸问题,因此设计了MCS锁。该锁实现的核心思想为:每个锁的申请者只能在本地CPU的变量上自旋,而不是全局的变量上。MCS锁本质上是一种基于链表结构的自旋锁。 排队自旋锁(OSQ锁) MCS锁机制会导致spinlock数据结构变大,在内核中很多数据结构内嵌了spinlock数据结构,这些数据结构对大小很敏感,这导致了MCS锁机制一直没能在spinlock数据结构上应用,只能屈就于互斥锁和读写信号量。在Linux 4.2内核中,基于排队自旋锁机制比基于排队机制在性能方面高20%,特别是在锁征用激烈的场景下,文件系统的测试性能会有116%的提升。OSQ非常适用于NUMA架构的服务器,特别是又大量的CPU内核且锁争用激烈的场景。 信号量         信号量允许无数据时,进程进入睡眠状态,其适用于一些情况复杂、加锁时间比较长的应用场景,如内核与用户空间复杂的交互行为等。 信号量的定义结构如下: <include/linux/semaphore.h> struct semphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };         从数据结构上看,信号量的本质是一个计数器count,当计数器为0时,消费端进入睡眠状态,直到生产端提升计数器计数时唤醒消费端处理消息。而wait_list表示的是当前在等待的消费者信息,lock则为保护count和wait_list数据准确性的锁。 信号量的使用方法: <include/linux/semaphore.h> static inline void sema_init(struct semaphore *sem, int val) // 将信号量sem初始化为val // down函数 (消费者) void down(struct semaphore *sem) // 在争用信号量失败是不进入可中断的睡眠状态 int down_interruptible(struct semaphore *sem) // 在争用信号量失败是进入可中断的睡眠状态 int down_killable(struct semaphore *sem) int down_trylock(struct semaphore *sem) // 返回0,表示成功获取锁;返回1,则表示获取锁失败 int down_timeout(struct semaphore *sem, long jiffies) // up函数 (生产者) void up(struct semaphore *sem)         使用场景:无法使用互斥锁的场景下使用 互斥锁         从功能理解上,互斥锁是个特殊的信号量(计数值只有0和1两个值的信号量)。而互斥锁引入的缘由,也是因为互斥锁相对于信号量更为简单轻便,在锁争用激烈的测试场景下,互斥锁比信号量执行速度更快,可扩展性更好,且数据结构定义比信号量小。 互斥锁的定义及使用: <include/linux/mutex.h> struct mutex { atomic_long_t owner; // 0表示未被持有,非0值则表示锁持有者的task_struct指针的值。其低三位有特殊含义: // #define MUTEX_FLAG_WAITERS 0x01 // 表示互斥锁的等待队列里有等待着,解锁的时候必须唤醒这些等候的进程 // #define MUTEX_FLAG_HANDOFF 0x02 // 对互斥锁的等待队列中的第一个等待着会设置这个标志位,锁持有这在解锁时把锁直接传递给第一个等待者 // #define MUTEX_FLAG_PICKUP 0x04 // 表示锁的传递已经完成 // #define MUTEX_FLAGS 0x07 spinlock_t wait_lock; // 用于保护wait_list睡眠等待队列 #ifdef CONFIG_MUTEX_SPIN_ON_OWNER struct optimistic_spin_queue osq; // 当发现锁持有者正在临界区执行并且没有高优先级的进程要调度时,当前进程坚信锁持有者会很快离开临界区并释放锁,因此与其睡眠 // 等待不如乐观自旋等待,以减少睡眠唤醒的开销 #endif struct list_head wait_list; }; //初始化 void mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key) #define DEFINE_MUTEX(mutexname) \ struct mutex mutexname = __MUTEX_INITIALIZER(mutexname) #define __MUTEX_INITIALIZER(lockname) \ {.owner = ATOMIC_LONG_INIT(0) \ , .wait_lock = __SPIN_LOCK_UNLOCKED(lockname.wait_lock) \ , .wait_list = LIST_HEAD_INIT(lockname.wait_list) } // 获取锁 void __sched mutex_lock(struct mutex *lock) // 释放锁 void __sched mutex_unlock(struct mutex *lock) 互斥锁使用场景: 同一时刻只有一个线程可以持有互斥锁 只有锁持有者可以解锁 不允许递归地加锁和解锁 当进程持有互斥锁时,进程不可退出 互斥锁必须使用官方的接口函数来初始化 互斥锁可以睡眠,因此不允许在中断处理程序或者中断上下部(tasklet、定时器等)中使用 读写锁 信号量有一个明显的缺点,没有区分临界区的读写属性。读写锁通常允许多个线程并发地读访问临界区,但写访问只限制一个线程。读写锁能有效地提高并发性,在多处理器系统中允许有多个读者同时访问共享资源,但写者是排他性的。 读写锁具有以下特性: 允许多个读者同时进入临界区,但同一时刻写者不能进入 同一时刻只允许一个写者进入临界区 读者和写者不能同时进入临界区 读写自旋类型 <include/linux/rwlock_types.h> typedef struct { arch_rwlock_t raw_lock; }rw_lock_t; <include/asm-generic/qrwlock_types.h> typedef struct qrwlock { union { atomic_t cnts; struct { u8 wlocked; u8 __lstate[3]; }; }; arch_apinlock_t wait_lock; } arch_rwlock_t; // 常用函数: rwlock_init() // 初始化rwlock write_lock() // 申请写者锁 write_unlock()// 释放写者锁 read_lock() // 申请读者锁 read_unlock() // 释放读者锁 read_lock_irq()//关闭中断并申请读者锁 write_lock_irq()//关闭中断并申请写者锁 write_unlock_irq()// 打开终端并释放写者锁   读写信号量 <include/linux/rwsem.h> struct rw_semaphore { long count;//表示读写信号量的计数 // 0x0000 0000:初始化值,表示没有读者和写者 // 0x0000 000X:表示有X个活跃的读者或者正在申请的读者,没有写者干扰 // 0xFFFF 000X:表示有可能有X个活跃读者,还有写者正在等待;或者表示有一个写者持有锁,还有多个读者在等待 // 0xFFFF 0001:表示当前只有一个活跃的写者;或者表示一个活跃或者申请中的读者,还有写者正在睡眠等待 // 0xFFFF 0000:表示WAITING_BIAS,有读者或者写者正在等待,但是它们都没成功获取锁 struct list_head wait_list; // 用于管理所有在该信号量上睡眠的进程,没有成功获取锁的进程会睡眠在这个链表中 raw_spinlock_t wait_lock; // 用于实现对count变量的原子操作和保护 #ifdef CONFIG_RWSEM_SPIN_ON_OWNER struct optimistic_spin_queue osq; struct task_struct *owner; //写者获取锁时,只想锁持有者的task_struct数据结构 #endif }; //常用接口 void down_read(struct rw_semaphore *sem) //申请读者锁,若一个进程持有读者锁,则允许继续申请多个读者锁,申请写者锁则需要等待 void down_write(struct rw_semaphore *sem)//申请写者锁,若一个进程持有写者锁,则第二个进程申请写者锁则需自旋等待,申请读者锁则需等待 void up_write(struct rw_semaphore *sem)//释放写者锁,若等待队列中第一个成员是写者,则唤醒该写者,否则,唤醒排在等待队列中最前面连续的几个读者 void up_read(struct rw_semaphore *sem)//释放读者锁,若等待队列中第一个成员是写者,则唤醒该写者,否则,唤醒排在等待队列中最前面连续的几个读者      RCU         RCU实现的目标:读者线程没有同步开销,或者说同步开销变得很小,甚至可以忽略不计,不需要额外的锁,不需要使用原子操作指令和内存屏障指令,即可畅通无阻地访问;而把需要同步地任务交给写者线程,写者线程等待所有读者线程完成后才会把旧数据销毁。 //常用接口 rcu_read_lock()/rcu_read_ublock() // 组成一个RCU读者临界区 rcu_dereference() //获取被RCU保护地指针,读者线程要访问RCU保护地共享数据,需要使用该函数创建一个新指针,并且只想被RCU保护的指针 rcu_assign_pointer() //通常用于写者线程。在写者线程完成新数据的修改后,调用该接口可以让被RCU保护的指针指向新创建的数据,用RCU的术语是发布了更新后的数据 synchronize_rcu() //同步等待所有现存的读访问完成 call_rcu() //注册一个回调函数,当所有现存的读访问完成后,调用这个回调函数销毁旧数据   内核中锁机制的特点和使用规则 锁机制 特点 使用规则 原子操作 使用处理器的原子指令,开销小 临界区的数据变量、位等简单的数据结构 内存屏障 使用处理器的内存屏障指令或GCC的屏障指令 读写指令时序的调整 自旋锁 自旋等待 中断上下文,短期持有锁,不可递归,临界区不可睡眠 信号量 可睡眠的锁 可长时间持有锁 读写信号量 可睡眠的锁,多个读者可同时持有锁,同一个时刻只能有一个写者,读者和写者不能同时存在 程度员定出临界区后读/写属性才有作用 互斥锁 可睡眠的互斥锁,比信号量快速和简洁,实现自旋等待机制 同一时刻只有一个线程可持有互斥锁,由锁持有者负责解锁,即同一个上下文中解锁,不能递归持有锁,不适合内核和用户空间复杂的同步场景 RCU 读者持有锁没有开销,多个读者和写者可同时共存,写者必须等待所有读者离开临界区后才能销毁相关数据 受保护资源必须通过指针访问,如链表等 相关代码详解             书中对以上各个模块的相关代码逻辑以及版本迭代过程有详细讲解,但受限于版面,涉及到linux内核源码部分,需自行查阅源码。             源码路径:文件列表 - runninglinuxkernel_5.0/runninglinuxkernel_5.0 - 公开仓库 (coding.net)

  • 2024-03-11
  • 回复了主题帖: 《奔跑吧Linux内核(第2版)卷2:调试与案例分析》- 初识本书及个人推荐阅读方式

    13620203064 发表于 2024-3-11 17:11 电子版在哪里? 先看看电子版的? 不知道容易不? 我也只有纸质版的,电子版还真不知道哪有

  • 2024-03-10
  • 发表了日志: 《奔跑吧Linux内核(第2版)卷2:调试与案例分析》- 初识本书及个人推荐阅读方式

  • 发表了主题帖: 《奔跑吧Linux内核(第2版)卷2:调试与案例分析》- 初识本书及个人推荐阅读方式

    背景介绍         自己平时工作内容集中在单片机和RTOS上,由于平时工作经常需要做算法落地,深感算力不够对算法落地所增加的难度。因此在工作之余,逐步开始了解linux这块的知识,以期在不久的将来,不再因为芯片算力不够而去采取降低算法指标来实现算法落地。         很感谢EEwrold和笨叔给予这次机会,能够给到这次机会来系统性的学习linux内核只是和常见问题的调试技巧。而这些知识在后续的工作中很可能经常用到。 初识本书         本书属于第二版发行,针对第一版发行时读者反馈的一系列问题,内核版本迭代中新增的特性,以及补足内核调试技巧的需求,笨叔进行了重新修订发行。从本书的目录结构上看,个人认为本书特别适合以下人群反复阅读: 有一定linux内核开发经验,但缺乏系统性了解的linux内核工程师 有一定rtos开发经验,希望更进一步学习了解linux工作原理,甚至上手进行linux系统开发的工程师 有linux求职需求的求职人员 目前正在学习linux系统的师生 个人推荐阅读方式 针对有Linux系统开发经验的人群         个人推荐方式为,搜索目录,选定与自己所需要查询问题相关度很高的标题并跳转至该章节阅读。此外,若目录中章节描述与想了解内容相差较大,可以尝试直接跳转至每一章的开始部分,此部分会留出“本章高频面试题”的问题列表,个人理解此部分内容为本章节主要知识点,可以通过这些知识点确定是否有所需要查询的内容。若搜索目录和章节开始部分都无法定位到自己想了解的知识点,那只有一种办法,粗看章节内容确认是否有想了解的信息,若有,则细看,若无,则查阅其他资料了解。 针对无linux系统开发经验的人群         首先,个人建议尽可能的从头读到尾,逐步加深的方式去学习。在这一点上,有个可惜的点,我曾经为了寻找到一个很好的看linux内核源码的工具,花费了两周时间,但还是没有找到特别合适的工具。而本书的开头提到,可以使用Source Insight或vim来查看linux源码,而这两个工具之前是被我否决的,被否定的原因是:Source Insight在文件数多时经常奔溃,且无法精确定位函数的跳转关系,另外,Source Insight的项目工程文件为十六进制存储的,无法编写工具直接将所使用的文件自动配置到工程中(目前看过的基于方案商的linux源码都比较乱,驱动和框架文件全部塞到 Drivers目录下,且不少文件直接塞到框架文件中,手动排除比较困难)。VIM的问题是,只能单个文件单个文件的查看和修改,跨文件间的函数跳转很难实现。但是呢,在阅读本书的“如何阅读本书”章节时,我发现笨叔有提到怎么使用Vim来阅读linux内核源码,可惜具体使用方法在《奔跑吧Linux内核入门篇》中,要更进一步的了解,只能通过看那本书实现了。         其次,最好搭建一个linux内核的阅读和实验环境,如笨叔推荐的QEMU虚拟机模拟的方式,在学习书本知识的同时,通过动手操作的方式加深理解。 最后         Linux内核发展了30多年,已经从一个只有不到1W行源码的小项目发展至如今这种无法用一个工程去查看linux内核全貌的程度。变成了一个看似入门简单,实则短时间内只能熟悉小部分模块,需要长时间的积累才能相对精通的大项目。个人认为,如何降低Linux学习和深入难度已经成了一个需要解决的问题。而在这个问题解决前,我们能做的是,根据前人的经验,从地基到各种框架,一层一层地学习和应用,逐步理解Linux的运行机制,以至于最终能够系统性的解决实际应用中Linux暴露出来的问题。

  • 2024-03-07
  • 回复了主题帖: 阅读打卡第一站:并发与同步—— 《奔跑吧Linux内核2:调试与案例分析》

    本帖最后由 oxlm_1 于 2024-3-8 12:25 编辑 1.在ARM64处理器中,如何实现独占访问内存?     1. ARM64处理器中,通过CAS指令(LSE扩展是否也是ARM64的特征?)来实现实现原子操作,进一步的实现独占内存访问。     2. 系统级的锁操作,临界区 2.atomic_cmpxchg()和atomic_xchg()分别表示什么含义? atomic_xchg(ptr, new): 原子地把new的值设置到ptr地址中并返回ptr地原值 atomic_cmpxchg(ptr, old, new):原子地比较ptr的值是否与old相当,若相等,则把new的值设置到ptr地址中,返回old的值 3.在ARM64中,CAS指令包含了加载-获取和存储-释放指令,它们的作用是什么?     加载-获取,是在加载前,确保之前所有的内存操作都已经完成,以保证对共享数据的读取是最新的     存储-释放,确保之后所有内存操作都已完成,以保证对共享数据的修改对其他线程可见

  • 2024-03-04
  • 回复了主题帖: 读书入围名单: 《奔跑吧Linux内核2:调试与案例分析》

    个人信息无误会确认可以完成阅读计划和打卡任务

  • 2024-02-23
  • 回复了主题帖: 2024开工大吉,你期待测评中心,能有哪些板卡或书籍等?

    希望有一些将zepyhr的书籍,这个操作系统在国内的资料太少了,入门难度有点高

  • 2024-01-25
  • 回复了主题帖: 领取审核名单(第五批): 辞旧,年底清仓,100+板卡来袭,有缘来领

    个人信息无误,已知晓需自己支付邮费

  • 2024-01-18
  • 回复了主题帖: 辞旧:年底清仓,100+板卡来袭,有缘来领

    申请板卡:沁恒电子ch554 领取理由:需要一个低成本方案实现HID透传(spi 转 hid)功能,目前找了一圈只有ch55x系列比较适合

最近访客

< 1/1 >

统计信息

已有13人来访过

  • 芯积分:78
  • 好友:1
  • 主题:3
  • 回复:17

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言