linux内核同步方法学习和总结:(如有误差或不同理解麻烦评论跟帖,十分感谢)


linux内核同步方法学习和总结:(如有误差或不同理解麻烦评论跟帖,十分感谢)
1.原子操作
  有原子整数、原子位操作(即对一个位或一个数操作)。原理是使用硬件lock指令,锁住和总线相连的该原子整数/位 对应的内存地址,紧接着操作该内存,譬如+1,-1或清0或取值。
  对于硬件来说无论有多少个处理器,该地址只有一个引脚和总线相连。(待找硬件的同时确认是否理解正确)
  另外,硬件lock指令只是锁住该地址内存总线,和硬件中断、软中断、中断下半部、进程上下文切换没关系,是两回事,不影响。
  但每次lock操作尽量在一个时钟中断内完成(一个进程切换周期)。由于lock指令操作的只是一个32/64位地址变量,所以不存在lock时间过长的问题。
  常用操作携带atomic字。
 
2.自旋锁
  spin lock关键字。
  多处理器中,只有一个处理器使用自旋锁占用临街资源,其他处理器都在忙等待(类似while反复尝试即自旋循环)。
  自旋锁使用注意:
   A.不能递归使用,假如自己占用资源,在锁过程中又去获取自旋锁,那么自己会一直在忙等待上一层锁,那么永远无法释放了。(有点类似于死锁的味道)
   B.自旋锁和中断的关系,自旋锁会被中断打断,假如自旋过程中被中断打断,在中断过程中有自旋锁访问相同临街资源,造成死锁。
     所以使用自旋锁时一定要禁止本处理器的中断,其他处理器处理中断如果要访问相同临街资源,那也只能自选忙等待,但不会死锁。
  如果在中断中使用自旋锁,也要先进行本处理的禁止、保存当前中断状态,因为现在操作系统都是支持中断抢占的。理由同上。
   C.软中断、中断下半部、以及进程上下文中使用自旋锁,都要注意互相可能产生死锁的情况,这些情况主要集中在同一处理器上。
     中断抢占优先级:硬件中断---->中断下半部(软中断或者TASKLET)---->进程上下文环境,对于不同级别环境下共享同一临界资源时,所以使用自旋时低级别的一定要禁止上面级别的打断。
     所以尽量避免不同级别的环境下共享相同临界资源。
 
  2.1自旋锁的引深应用(读写自旋锁)
    即只要没有写操作锁,可以同时支持多个读锁并发操作。主要应用于写操作比较少,但是读操作比较频繁的情况。当有任何一个读操作时,写操作必须忙等待,有且只有没有任何读操作进行时才能写锁操作。
 使用时要做好设计防止写操作长时间出于饥饿状态,有时能大大提高性能,有时能大大降低性能。
 
3. 信号量
   是一种睡眠锁,当资源不可用时导致睡眠,资源可用时唤醒。所以只能在进程上下文使用。信号量无法禁止中断,所以允许被打断或抢占。
   由于自旋锁能在进程上下文中使用,所以在自旋锁操作过程中,不能使用信号量,这样会导致进程睡眠。容易导致同一临界资源的其他cpu或高级别为了获取自旋锁始终处于忙等待。
   信号量可以有无数个等待资源释放者(排队),但每次释放资源只能唤醒睡眠队列中的优先级最高的一位。
  
  3.1 信号量的简化(互斥体)
   (原理同信号量,简化版信号量)
   只是二选一的关系,等待着始终是一个。
  
  3.2 信号量的引深(读写信号量)
    (原理同信号量)
 只要没有读信号量锁住,可以支持多个并发读锁锁住临界资源。但和读写自旋锁存在同样的问题:只要有一个读锁存在,写锁必须睡眠,设计时候需要避免写锁一直处于饥饿状态。
 
4.完成变量
  原来类似信号量。
  A线程执行流完成,给B线程发送唤醒信号。B线程在被唤醒前waitfor该完成变量。
  这个过程有点类似于生产者消费者模型。
  
  
5.BLK大内核锁
  早期的内核中提供的机制,不做学习和研究。
6.顺序锁
  读锁读临界资源时,先读出锁计数值A,然后读出数据,再读一次锁计数B,若A==B表示此次读过程中没有写操作。若A!=B表示此次读过程中有写操作,重新读取重复之前的操作。
  顺序锁有利于写操作,无论是否有读锁锁住资源,写操作随时可以锁住资源修改数据(因为读锁可以判断,能够保证读值正确),并将锁计数++操作。
  适用于读者多,写者少的场景。
  适用于希望写优先级高于读的场景,即不允许写饥饿。
 
 
7.内核抢占
  优先级task A > task B.
  B在自旋锁中修改临界资源,但此时被该CPU的task A抢占了,这样会导致其他共享B临界资源的锁处于长时间忙等待,如果A中在访问临界资源可能造成死锁。这里不仅仅使用自旋锁其他锁机制也是同样适用。
  这就需要在锁的过程中禁止内核抢占,即使用preempt_disable(),对该函数中的技术count++操作,退出锁时进行preempt_enable()进行count--。只有当count==0时(preempt_count()获得)才会允许内核抢占。
  linux禁止内核抢占的几种情况:
  (1).内核正进行中断处理。在Linux内核中进程不能抢占中断(中断只能被其他中断中止、抢占,进程不能中止、抢占中断),在中断例程中不允许进行进程调度。进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错信息.
  (2).内核正在进行中断上下文的Bottom Half(中断的底半部)处理。硬件中断返回前会执行软中断,此时仍然处于中断上下文中.
  (3).内核正在执行调度程序Scheduler。抢占的原因就是为了进行新的调度,没有理由将调度程序抢占掉再运行调度程序。
  (4).内核正在对每个CPU“私有”的数据结构操作(Per-CPU date structures)。在SMP中,对于per-CPU数据结构未用spinlocks保护,因为这些数据结构隐含地被保护了(不同的CPU有不一样的per-CPU数据,其他CPU上运行的进程不会用到另一个CPU的per-CPU数据)。但是如果允许抢占,但一个进程被抢占后重新调度,有可能调度到其他的CPU上去,这时定义的Per-CPU变量就会有问题,这时应禁抢占。
 
 
8.顺序和屏障
  有的编译器和处理器会编译阶段进行优化对指令进行重新排序,有的cpu加载指令时也会存在加载重新排序问题(X86处理器保证不会进行重排序)。这样导致原先执行指令的顺序发生变化,进而产生各种异常。
  如:
 
    代码1:
 int b = 1;
 b++;
 int *p = &b;
 int a = *p;
 
  有可能加载到处理器后的执行顺序为:先执行int *p = &b;后执行b++。这样*p的取值并不是我们期望的。
  所以需要修改成:
 
 int b = 1;
 b++;
 mb();//保证b++和int *p = &b;的顺序不会发生颠倒。即mb()前后的顺序进行严格控制。
 int *p = &b;
 read_barrier_depends();//保证前后执行序列,也可以使用rmb(),但是barrier执行更快。
 int a = *p; 
 
 
参考资料:《linux内核设计和实现(第三版)》

猜你喜欢

转载自blog.csdn.net/m0_37570820/article/details/80484521