linux下的竞态和并发都是比较容易发生的,原子操作只能针对整数进行,因此互斥和自旋锁也就被时常用来保护临界资源以解决竞态和并发的问题。
互斥体和自旋锁用法:
在进程上下文中,保持时间长的情况可以用互斥体,因为可以睡眠,保持时间短的可以用自旋锁,减少上下文的切换时间。中断上下文中可以用自旋锁,不能用互斥体,因为可能会睡眠。但是中断上下文获取自旋锁之前要先禁用本地中断,防止中断嵌套。有自旋锁的代码不能睡眠,并且不能用在递归,否则会引起死锁。
信号量:
信号量是操作系统最典型用于互斥和同步的手段,它的值可以是0、1或者n。可以执行P操作和V操作,又是原子操作,在执行时,不会被中断。
P(s):将s的值减 1,如果 s >= 0, 进程继续执行,否则将进程设为等待状态,进入睡眠,加入等待队列。也就是down操作
V(s):将s的值加 1 ,如果 s > 0, 唤醒队列中等待信号量的进程。也就是UP操作。
- struct semaphore sem; 信号量的定义
- void sema_init(struct semaphore *sem,int val):初始化,并设置信号量sem的值为val。
- void down (struct semaphore *sem );信号量减1,会睡眠,不能在中断上下文使用。
- int down_interruptibleistruet semaphore * sem);与down函数一致,但其进人睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非0。
- int down_trylock (struct semaphore* sem);该函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回非0值。它不会导致调用者睡眠,可以在中断上下文中使用。
- vaid up (struct semaphore * sem ) 释放信号量 sem,唤醒等待者。
作为互斥使用时,进程1:P(s)---> 临界区--->V(s)。但linux内核比较倾向于mutex作为互斥手段。
作为同步使用时:--->进程1 执行down(p)等待----> 进程2:up(v)----->进程1执行:
进程1 进程2 down(p) 代码 等待 up(v) 代码
自旋锁:
自旋锁是一种典型对临界资源互斥的一种手段。为了获得一个自旋锁,会在某个cpu上先运行一个原子操作,这个操作是测试并设置某个内存变量,测试结果表示锁空闲,程序获得这个自旋锁,并继续执行临界资源,如测试结果表示被占用,程序将原地打转,不断循环的获取该锁是否空闲,直到该锁空闲后,在往下继续执行临界资源。
- spinlock_t lock; //定义自旋锁
- spin_lock_init (lock) //初始化自旋锁
- spin_lock (lock) //获得自旋锁,获得立即返回,否则原地打转,直到释放返回为止
- spin_unlock ( lock) //释放自旋锁
互斥体:
可以对临界资源的互斥,是linux内核常用的互斥一种手段。与信号量的互斥是一样的,都是会互斥之后会休眠,用法与信号量的互斥用法一致,都是用于进程上下文。
- struct mutex my_mute //互斥体定义
- mutex_init{&my_mutex.); //初始化
- void mutex_lock(struct mutex *lock); //获取互斥体
- void mutex_unlock(struct mutex*lock); //释放互斥体
完成量:
完成量的的作用可以用来作同步使用,一个进程等待另外一个进程完成某任务后继续执行:
- struct completion my_completion; // 完成量的定义
- init_completion ( &my_completion); //初始化完成量,初始值为0
- void wait_for_completion (struct completion *c); //等待完成量被唤醒
- void complete (struct completion *e); //唤醒是时候
进程1 进程2 代码 wait_for_completion complete ....等待 继续执行