Linux:并发与同步(一)

本博客将深入探讨Linux中的并发与同步机制,从原理到实践,逐步揭示其中的奥秘。我们将介绍Linux中常见的并发编程概念,例如进程、线程、锁,以及它们在实际应用中的使用技巧和最佳实践。

Linux:并发与同步(二)

​ 并发访问,指的是多个内核代码路径同时访问和操作数据,这个代码可执行路径有以下几种情况:

  • 内核执行路径
  • 中断处理程序
  • 内核线程

​ 临界区指的是访问和操作共享数据的代码段,其中的资源不能被多个执行线程访问。为了防止并发访问,就需要保证访问临界区的原子性,即在临界区内不能有多个并发源同时执行。

​ 在内核中产生并发访问的并发源主要有以下几种,但是分单处理器系统和多处理器

1.中断和异常:中断发生后,中断处理程序和被中断的进程之间可能产生并发访问。

  • 单核:中断处理程序可以打断软中断和tasklet的执行。
  • 多核:同一类型的中断处理程序不会并发执行,但是不同类型的中断可能送达不同的cpu,不同类型的中断处理程序可能会并发执行。

2.软中断和tasklet:软中断和tasklet随时可能被调度,从而打断当前正在执行的进程上下文。

  • 单核:软中断和tasklet之间不会并发执行,但是可以打断进程上下文的执行。
  • 多核:同一类型的软中断会在不同的CPU上并发执行,同一类型的tasklet是串行执行的,不会在多个CPU上并发执行。

3.内核抢占:调度器支持抢占,会导致进程和进程之间的并发访问。

  • 单核:只有在支持抢占的内核中,进程上下文会产生并发。
  • 多核:不同CPU上的进程上下文会并发执行。

思考清楚哪些地方是临界区,该使用什么机制来保护临界区。

1.1 原子操作

顾名思义,即指令以原子的方式执行,执行过程不会被打断。

​ 经典的例子就是两个CPU并发的访问同一个静态变量对其同时加一,从CPU的角度来看,其过程是:①读取变量的值并存储到通用寄存器中②在通用寄存器里做i++运算③写会变量所在的内存。如果上述操作同时进行,那么就会发生并发访问。

​ 要怎么解决这种情况?

​ 很多人都会想到加锁,但是加锁操作会导致较大的开销。故内核提供了atomic_t类型的原子变量,其实现依赖于不同的架构:

#ifdef CONFIG_64BIT
typedef struct {
   
	s64 counter;
} atomic64_t;
#endif

在内核中,原子函数就像一条汇编语句,能够原子的完成上述的“读-修改-回写”机制。

1.2 内存屏障

​ 内存屏障,确保多线程环境下的内存访问顺序正确,是CPU或者编译器在对内存随机访问的操作中的一个同步点,只有在此点之前的所有读写操作都执行后才可以执行此点之后的操作。

​ 能够实现高效的无锁数据结构,提高多线程程序的性能表现。

1.2.1为什么会出现内存屏障?

​ 在学习内存屏障之前,先了解一下有序处理器和无序处理器的指令处理过程:

  • 有序处理器严格按照指令的顺序执行,这意味着每条指令都必须等待前一条指令完成所有阶段后才能开始。这种设计简单但效率较低,因为处理器在cache miss时会等很久。
  • 无序处理器允许指令在操作数可用时立即执行,而不必等待前面的指令完成。

​ 如果CPU需要读取的地址中的数据已经已经缓存在了cache line中,即使是cpu需要对这个地址重复进行读写,对CPU性能影响也不大,但是一旦发生了cache miss,如果是有序处理器,CPU在从其他CPU获取数据或者直接与主存进行数据交互的时候需要等待不可用的操作对象,这样就会非常慢,非常影响性能。举个例子:

​ 假设CPU0发起对某个内存地址的写操作,但是该地址的数据存放在CPU1的缓存中。

  1. CPU0写操作
    • CPU0准备写某个内存地址,但该地址的数据不在CPU0的缓存中,而是在CPU1的缓存中。
  2. 发送无效化消息(Invalidate Message)
    • CPU0会发出一个无效化消息(invalidate message),使其他CPU(包括CPU1)的缓存中该地址的数据无效。这是为了确保写操作时数据的一致性。
  3. 等待无效化完成
    • 在有序处理器中,CPU0必须等待所有相关CPU完成无效化操作后,才能进行写操作。
    • 在乱序处理器中,CPU0不需要等待所有无效化完成。相反,它会将无效化消息放入无效化队列(invalidate queues),并继续执行其他指令,提高了CPU的并行处理能力和整体性能。

​ 但也带来了一个问题,就是程序执行过程中,可能会由于乱序处理器的处理方式导致内存乱序,程序运行结果不符合我们预期的问题。所以我们需要内存屏障来解决。

1.2.2 内存屏障的分类

​ 内存屏障能够让编译器或CPU在内存上访问有序,主要包括两类:

编译器内存屏障

Linux 内核提供函数 barrier() 用于让编译器保证其之前的内存访问先于其之后的完成。

#define barrier() __asm__ __volatile__("" ::: "memory")

​ 由于编译器对代码进行优化时,可能会改变实际执行指令的顺序。避免这种行为的办法就是使用编译器屏障(又叫优化屏障)。例如:

#include <stdio.h>
int x,y,r;
void f ()
{
    
    x = r; 
    y= 1;
}

​ 我们使用gcc -O2 -S test.c,优化编译,得到其相关编译代码如下:

movl r(%rip), %eax 
movl $1, y(%rip) 
movl %eax, x(%rip)

​ 可以清楚地看到经过编译器优化之后,movl $1, y(%rip)先于movl %eax, x(%rip)执行,这意味着,编译器优化导致了内存乱序访问,现在将内存屏障加在两句代码之间:

#include <stdio.h>

int x,y,r;
void f ()
{
    
    x = r; 
    __asm__ __volatile__( "" : : : "memory" );
    y= 1;
}

​ 编译过的代码就不会出现内存乱序访问了。

CPU内存屏障
 */
#define mb()	asm volatile("lock; addl $0,0(%%esp)" ::: "memory")
#define rmb()	asm volatile(
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值