PCIE(RIFFA)驱动解读(四)
来源:AriesOpenFPGA 发布时间:2023-01-08 分享至微信

Part5Linux并发与竞态概述

15.1 并发与竞争

Linux是个多任务操作系统,肯定会发生同一时间内两个任务操作同一内存或同一设备的情况,这就是竞争,在大部分情况下这种竞争是不被允许的。能够被多个任务和中断访问到的资源称为共享资源, 对于共享资源的访问需要保护。

Linux中的并发主要有以下几个原因:

  1. 多线程并发

  2. 抢占式并发,2.6内核后的版本引入抢占机制,其他任务可以打断当前正在执行的任务。

  3. 中断引起的并发,中断是优先级很高的任务。

  4. SMP并发,现代处理器中都是多核,多核CPU存在核间并发访问。

为什么要保护共享资源?比如一个存储空间,两个线程同时往里面写,那到底写进去的是哪个线程的数据?所以需要对这个存储空间进行保护,每次只允许一个线程进行读写,别的线程等着。

Linux中处理竞争的方法有原子操作、自旋锁、信号量、互斥体等。

25.2 原子操作

原子操作是指不能再进一步分割的操作,就像原子一样不可割裂,一般用于变量操作或位操作。

以ARMv7为例,在C语言中,一条"num=8;"的语句在编译后会形成三条汇编指令,如下所示:

第一条汇编语句是将num这个变量在内存中的地址存入通用寄存器r0中,第二条是将立即数8存入通用寄存器r1中,第三条是将r1中的数据存入内存中r0的位置。

这三条指令在CPU中是一条一条执行的,也就是说他们其实不是一个整体,而是可以割裂成三条;假如有两个线程,一个执行num=8,一个执行num=10,那么可能这六条汇编语句是穿插着执行的,最后不一定num中到底是多少。

所以需要让每条语句的三条汇编指令成为一个不可割裂的整体,也就是让这三条指令成为一个原子,要执行一起执行,否则就都不执行,这就需要用到原子变量和原子操作了。

原子操作其实是靠硬件实现的,也就是有对应的指令,称为原子指令,在ARMv7中ldrex和strex就是两条原子指令,如果把这两条指令替换掉上面那三条,就只能有一个线程的操作能成功(先执行strex的),没成功的再从ldrex开始执行一遍。

Linux提供了一种原子变量,定义在/linux/types.h中,如下所示,其实就是一个int型变量放到结构体中了:

也提供了对原子变量的操作API,例如int atomic_read(atomic_t *v)就是读v的值;void atomic_set(atomic_t *v, int i)就是向v写入i等等,就不一一介绍了。

35.3 自旋锁

5.3.1 自旋锁的优缺点

原子变量只能操作整形,局限太大了,而自旋锁可以操作很多东西。自旋锁就是一把锁,当一个线程访问临界区(共享资源)前,首先上锁,然后开始操作临界资源,此时若另一个线程也来访问临界区,那么当它上锁时会发现锁正在被用,那么这个线程就会原地轮询,直到锁被第一个线程用完且归还后才轮到这个线程上锁然后访问临界区。

自旋锁的特点是当线程获取锁失败时不会阻塞,而是原地轮询。

这样处理的好处是如果当锁很快就会被归还时(轻量级加锁),使用自旋锁效率较高,因为操作系统将任务从休眠态转换到运行态是比较耗费CPU资源的,如果上锁时间比较短,那么其实没必要阻塞线程,阻塞线程反而会对资源有更大的开销。

自旋锁的缺点是如果当别的线程持有锁的时间较长,因为自旋锁是不阻塞的,CPU一直运行这个线程,但这个线程又一直在轮询锁的状态,很浪费CPU资源。

5.3.2 自旋锁的死锁

Linux 2.6内核以后引入可抢占功能。

不可抢占式内核是指高优先级的进程不能中止正在内核中运行的低优先级的进程而抢占CPU运行。进程一旦处于内核态(例如用户进程执行系统调用),则除非进程自愿放弃CPU,否则该进程将一直运行下去,直至完成或退出内核。

可抢占式内核指即当进程位于内核空间时,有一个更高优先级的任务出现时,如果内核允许抢占,则可以将当前任务挂起,执行优先级更高的进程。

如果是单核处理器,使用非抢占内核,当进程处于内核态的临界区时,已经上了锁,如果此时来了一个中断且中断里也要给自旋锁上锁,那么CPU就会死在中断里出不来;如果进程在临界区时使用睡眠函数sleep自愿阻塞,且新被调度的进程也想使用自旋锁,那么CPU也会在这个新进程中死循环,所以Linux没有为单核非抢占内核提供自旋锁。

使用抢占式内核也可能会被中断打断进入死锁,所以用自旋锁访问临界区时要先关中断。

RIFFA驱动中没有用到自旋锁,这里就不讲它的API了。

45.4 信号量

5.4.1 信号量(semaphore)

RTOS中常用到信号量进行互斥访问,SystemVerilog中也有信号量的概念,称为旗语(其实是同一个单词)。

自旋锁是只有一把锁,而信号量是可以有一把也可以有多把,不过常用的就只用一把锁,信号量和自旋锁的区别是当全部信号量上锁后再有进程试图上锁则该进程阻塞,而自旋锁是轮询不是阻塞,所以信号量适用于持锁时间长的临界区访问。

值得注意的是,信号量可不管临界区里的操作是写是读,全给你阻塞了,这种无差别的阻塞可能会降低系统性能。

RIFFA中使用的是信号量的升级版­---读写信号量(rwsem)。

5.4.2 读写信号量(rwsem)

不是所有临界区都需要互斥的,以双口RAM举例,双口RAM中有写-写冲突和读-写冲突,但没听说过有读-读冲突吧,因为两个读同一地址的操作根本不会影响读取结果的正确性,信号量也是,如果临界区里是读操作,那么没必要阻塞它,不会产生读取错误,而读和写、写和写就得阻塞了,这就是读写信号量的原理。读写信号量因为不会阻塞读-读操作,所以提高了并发程度,进而提高了系统的性能。

读写信号量的类型是struct rw_semaphore结构体,在/linux/rwsem.h中定义,函数down_read是以读者身份上锁,另一个读者任务使用这个函数获取锁时不会被阻塞;函数up_read是以读者身份还锁。down_write函数是以写者身份上锁,另一个线程使用不管是读者down还是写者down都会被阻塞;函数up_write是以写者身份还锁。

RIFFA驱动程序里只使用了read类型的操作函数,是读者身份。

55.5 互斥体

互斥体其实就是只有一把锁的信号量,还是那句话,互斥体更专业。RIFFA驱动程序里没有用到互斥体,这里就不展开讲了。


[ 新闻来源:AriesOpenFPGA,更多精彩资讯请下载icspec App。如对本稿件有异议,请联系微信客服specltkj]
存入云盘 收藏
举报
全部评论

暂无评论哦,快来评论一下吧!