|
|
EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
. e7 O$ s. X# f% v+ @* G2 `) i存在共享资源(共享一个文件,一块内存等等)的时候,为了防止并发访问时共享资源的数据不一致,引入了同步机制。( U& `( G: Z- F4 ]+ Z; T
1 z& u1 @% L$ U
主要内容:
) ~1 v2 u1 ]+ ` y% j
# ^( w( \/ W. f- F2 `同步的概念: _% X" o- V8 N( h( ~
同步的方法-加锁
$ S3 X* _3 m3 g2 L# Z死锁& X8 ^2 x6 X+ k6 E& c D+ q
锁的粒度
1 \. R- V# p9 F* I* v# a. U
- d n3 A) d* @2 |3 m, B1 l, c" V+ q! Z/ o/ c a: o
1. 同步的概念
& f/ B, A' g9 \; G4 F, T# f7 J/ k了解同步之前,先了解另外2个概念:
1 b5 Z* ] N$ x2 Y" }7 ^$ D+ L$ C3 Q/ t- P/ Z ?
临界区 - 也称为临界段,就是访问和操作共享数据的代码段。
5 N2 m& {5 d6 a3 x竞争条件 - 2个或2个以上线程在临界区里同时执行的时候,就构成了竞争条件。
3 X3 `; ?/ l$ S- D2 h- W! b 8 N. J' `4 h9 @, O6 L
/ u! Z' e; l, g" y
所谓同步,其实防止在临界区中形成竞争条件。8 M9 s/ E2 z. X5 X& w9 \3 F
+ C: l, K( R9 e2 V8 o* P* M$ k, f
如果临界区里是原子操作(即整个操作完成前不会被打断),那么自然就不会出竞争条件。
6 X. h% D, o6 E3 ^) ?$ Q/ ]0 D' ~" B' p& p& g' J+ y
但在实际应用中,临界区中的代码往往不会那么简单,所以为了保持同步,引入了锁机制。
+ N5 b' k7 `& n6 s( L$ ^$ b- z2 Y U) `& H4 [- x
Q f" f* X, h5 J: d# q
+ D7 U8 z: K) K3 }# H7 |2. 同步的方法-加锁
9 j2 }+ t2 [' B8 _5 T0 d为了给临界区加锁,保证临界区数据的同步,首先了解一下内核中哪些情况下会产生并发。0 X4 y: a' g J1 F
S6 N' f \+ m3 l
6 i. @# S) s' f l m
0 {4 u0 u5 T" [+ Z) X1 X内核中造成竞争条件的原因:& t2 v, x& |1 n. d+ f
2 ~- f# D: y! g+ ~* ~
竞争原因) N2 p$ O B# Z& B
4 o7 D a% H0 a, M _% I
说明3 g1 Q2 s& A! `* r2 D7 @ j3 _
+ l6 l3 C0 H4 u. W) k+ s8 z7 W. w2 F! }中断 中断随时会发生,也就会随时打断当前执行的代码。如果中断和被打断的代码在相同的临界区,就产生了竞争条件
) _' Z! n' f6 X# U/ N4 U5 n软中断和tasklet 软中断和tasklet也会随时被内核唤醒执行,也会像中断一样打断正在执行的代码
9 G. y. |4 c3 E% x内核抢占 内核具有抢占性,发生抢占时,如果抢占的线程和被抢占的线程在相同的临界区,就产生了竞争条件, C! P# {7 L: ^9 H+ R0 D
睡眠及用户空间的同步 用户进程睡眠后,调度程序会唤醒一个新的用户进程,新的用户进程和睡眠的进程可能在同一个临界区中
! f$ v3 A+ g- D! b8 Y, Q. U对称多处理 2个或多个处理器可以同时执行相同的代码0 M- O" R B7 v0 h
- [# Q2 f, U9 U: A* E+ m3 F: G% N9 O$ n
为了在编写内核代码时避免出现竞争条件,在编写代码之前就要考虑好临界区在哪,以及怎么加锁。
% ]! A, G$ y5 w/ r8 l3 |; _9 m: d, H) Y9 z* f# M
在编写完代码后再加锁是非常困难的,很可能还会导致部分代码重写。
2 Y: Q- r! |' y" ^+ _- u( J h
* b2 C' V: V. Q5 V# Q& ~$ @, M. g * ^" g; I2 U( m7 C6 d: b
' C" j {/ L% G" F0 a
编写内核代码时,时时记着下面这些问题:' h) p# g3 C+ _- X# v. l% ~5 n0 D6 s) ~
6 i7 X2 `! y4 R/ C6 Y7 _
这个数据是不是全局的?除了当前线程以外,其他线程能不能访问它?
( o v/ |, L7 O9 i4 \) x( X0 s这个数据会不会在进程上下文或者中断上下文中共享?它是不是要在两个不同的中断处理程序中共享?2 d) j7 G: v. [# r3 h' z. N
进程在访问数据时可不可能被抢占?被调度的新程序会不会访问同一数据?
; _( x: L1 i- ?% b当前进程会不会睡眠(或者阻塞)在某些资源上,如果是,它会让共享数据处于何种状态?
9 G& W! O8 d9 }/ P. `7 y怎样防止数据失控?
$ V/ v; H" _. T- j- Y4 y e }% b5 C如果这个函数又在另一个处理器上被调度将会发生什么?
( z. a' c, v0 W6 D7 S8 `
8 f9 Z% @1 m# |0 u6 ], P% X" T/ `
& k! E# X* y1 {4 H9 m3. 死锁
/ |* r3 ~) d6 } x1 L7 t死锁就是所有线程都在相互等待释放资源,导致谁也无法继续执行下去。+ b( W. ]5 f. ~2 ?( ?7 O
; ? ?+ Y% I. ]下面一些简单的规则可以帮助我们避免死锁:
2 f; M6 d( {) F: b% w) p- p5 v7 B& {2 o5 n! {. _3 q
如果有多个锁的话,尽量确保每个线程都是按相同的顺序加锁,按加锁相反的顺序解锁。(即加锁a->b->c,解锁c->b->a)
: P% t& U# K" B- d; |/ R$ @2 U& m防止发生饥饿。即设置一个超时时间,防止一直等待下去。
: o3 C5 ~8 }! ?* Z! w, i/ x不要重复请求同一个锁。7 z6 Q7 `; s W W& i
设计应力求简单。加锁的方案越复杂就越容易出现死锁。% F6 G) w. P1 W6 B- [' S5 Q
9 g, a/ ]8 l: {. f
5 q, a8 m3 R3 I t& {6 ]4. 锁的粒度9 i. {6 h5 A2 y. {! W; U+ i. K
在加锁的时候,不仅要避免死锁,还需要考虑加锁的粒度。$ B; \/ v% T; S" `
4 M8 G* }" l. S2 D4 G; P4 P锁的粒度对系统的可扩展性有很大影响,在加锁的时候,要考虑一下这个锁是否会被多个线程频繁的争用。
6 ]& Z9 C7 \' `0 }# Q4 j& w+ _3 K
6 E, V; G$ h9 h/ F如果锁有可能会被频繁争用,就需要将锁的粒度细化。, p5 a8 P J. s% d/ g5 f
|8 z, d" P+ b6 f, Z6 a
细化后的锁在多处理器的情况下,性能会有所提升。
7 }0 u$ ?' ^+ B$ p( ^! ^+ T- M3 @: B5 I; X* Z, N
; \5 ]( [' G1 i
0 a" I/ j% Q& c& k6 X5 U
举个例子说明一下:比如给一个链表加锁,同时有A,B,C 3个线程频繁访问这个链表。8 ~6 L9 u& c1 n9 z# H
8 s$ o! e! C. }# \. R) L! i
那么当A,B,C 3个线程同时访问这个链表时,如果A获得了锁,那么B,C线程只能等待A释放了锁后才能访问这个链表。4 R" q" W, _- z7 }" d. Y. q
; {& z7 t2 \/ ^
4 D5 l1 S! @! D! e2 e
; W" ]7 P8 `, l& W# H2 y5 I4 q0 r如果A,B,C 3个线程访问的是这个链表的不同节点(比如A是修改节点listA,B是删除节点listB,C是追加节点listC),* B) g' v$ F( [- b
; i* Z0 A. k. d; Y: C
并且这3个节点不是连续的,那么3个线程同时运行是不会有问题的。) z( }' B, A: w7 i4 }
+ d# x/ `" J+ Z( c: G 9 K8 j% @5 I" _. z
4 ]3 Y) m1 y" v$ e6 L% d这种情况下就可以细化这个锁,把加在链表上的锁去掉,改成把锁加在链表的每个节点上。(也就是锁粒度的细化)
4 e% V& ~: H$ v* U1 {9 @) i" s
: R p+ P9 P! P; O那么,上述的情况下,A,B,C 3个线程就可以同时访问各自的节点,特别是在多处理器的情况下,性能会有显著提高。
9 z! i/ @# G6 B2 p* G4 V z, j9 P
5 F: @8 ]0 s1 g; Q" M/ a# S
4 y9 ~3 _" r1 I: n4 x6 s
( B! K2 ]7 |! R8 v- h) A, X* |最后还有一点需要提醒的是,锁的粒度越细,系统开销越大,程序也越复杂,所以对于争用不是很频繁的锁,就没有必要细化了。 |
|