并发与竞争
并发
多个“用户”同时访问同一个共享资源。
竞争
并发和竞争的处理方法
处理并发和竞争的机制:原子操作、自旋锁、信号量和互斥体。
1、原子操作
原子操作就是指不能再进一步分割的操作,一般原子操作用于变量或者位操作。
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量 。
2、自旋锁
Linux 内核使用结构体 spinlock_t 表示自旋锁 。对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。
注意:中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生。
3、信号量
信号量常常用于控制对共享资源的访问。
相比于自旋锁,信号量可以使线程进入休眠状态 。
使用信号量会提高处理器的使用效率。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。
总结一下信号量的特点:
①、因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
②、因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
③、如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于 1,此时的信号量就是一个二值信号量。
4、互斥体
在 FreeRTOS 和 UCOS 中也有互斥体,将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。
互斥访问表示一次只有一个线程可以访问共享资源,不能递归申
请互斥体。
mutex 的时候要注意如下几点:
①、 mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
中断
中断的处理方法:
①、使能中断,初始化相应的寄存器。
②、注册中断服务函数,也就是向 irqTable 数组的指定标号处写入中断服务函数
③、中断发生以后进入 IRQ 中断服务函数,在 IRQ 中断服务函数在数组 irqTable 里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。
获取中断号 -> 申请中断 -> 中断使能 -> 中断处理函数 -> 释放中断 -> 中断禁止
为实现中断处理函数的快进快出:
**上半部:**上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?
Linux 内核提供了多种下半部机制:软中断 、tasklet 、工作队列 。
软中断、tasklet和工作队列都是用于处理中断事件的机制,但它们的优先级和执行上下文有所不同。软中断的优先级最高,可以在任何上下文中被调度执行;tasklet的优先级次之,它在软中断上下文中执行,可以睡眠但不能被抢占;工作队列的优先级最低,它在进程上下文中执行,可以睡眠也可以被抢占。根据具体的需求和性能要求,可以选择合适的下半部机制来处理中断事件。
阻塞IO和非阻塞IO
阻塞式 IO :会将应用程序对应的线程挂起,放到等待队列当中,直到设备资源可以获取为止。
非阻塞 IO:应用程序对应的线程不会挂起,它会通过poll函数不断的轮询,查看驱动设备资源可以使用。
这两种方式都需要应用程序主动的去查询设备的使用情况。
poll、 epoll 和 select 可以用于处理轮询,应用程序通过 select、 epoll 或 poll 函数来
查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调
用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行,因此需要在设备驱动
程序中编写 poll 函数。
异步通知
当驱动程序可以访问时,驱动可以主动向应用程序发送信号的方式来报告自己可以访问了,应用程序获取到信号以后就可以从驱动设备中读取或者写入数据了。
整个过程就相当于应用程序收到了驱动发送过来了的一个中断,然后应用程序去响应这个中断,在整个处理过程中应用程序并没有去查询驱动设备是否可以访问,一切都是由驱动设备自己告诉给应用程序的。
设备节点:根节点“/”下
pinctrl结点: iomuxc 节点下
常规操作:先设置某个 PIN 的复用功能、速度、上下拉等,然后再设置 PIN 所对应的 GPIO。
其实对于大多数的 32 位 SOC 而言,引脚的设置基本都是这两方面,因此 Linux 内核针对 PIN 的配置推出了 pinctrl 子系统,对于 GPIO
的配置推出了 gpio 子系统。
传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置
方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。
pinctrl 子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始化工作均由 pinctrl 子系统来完成, pinctrl 子系统源码目录为 drivers/pinctrl。
// 设备树中添加pinctrl节点模板
// 1、创建对应的节点
// 同一个外设的PIN都放在一个节点里面,在iomuxc节点中的“imx6ul-evk”子节点下添加“pinctrl_test”节点
pinctrl_test: testgrp{
/*具体的PIN信息*/
};
// 2、添加“fsl,pins”属性来保存信息
// pinctrl 驱动程序是通过读取“fsl,pins”属性值来获取 PIN 的配置信息
pinctrl_test: testgrp{
fsl,pins=<
/*设备所使用的PIN配置信息*/
>;
};
// 3、在“fsl,pins”属性中添加PIN配置信息
pinctrl_test: testgrp{
fsl,pins=<
MX6UL_PAD_GPIO1_IO00__GPIO1_IO00 config /*config是具体设置值*/
>;
};
如果 pinctrl 子系统将一个 PIN 复用为 GPIO 的话,那么接下来就要用到 gpio 子系统了。 gpio 子系统就是用于初始化 GPIO 并且提供相应的 API 函数,比如设置 GPIO为输入输出,读取 GPIO 的值等。 gpio 子系统的主要目的就是方便驱动开发者使用 gpio,驱动开发者在设备树中添加 gpio 相关信息,然后就可以在驱动程序中使用 gpio 子系统提供的 API函数来操作 GPIO, Linux 内核向驱动开发者屏蔽掉了 GPIO 的设置过程,极大的方便了驱动开发者使用 GPIO。
//I.MX6ULL-ALPHA 开发板上的 UART1_RTS_B 做为 SD 卡的检测引脚, UART1_RTS_B 复用为 GPIO1_IO19,通过读取这个 GPIO 的高低电平就可以知道 SD 卡有没有插入。
// 设置pinctrl 节点:将 UART1_RTS_B 这个 PIN 复用为 GPIO1_IO19,并且设置电气属性。
pinctrl_hog_1: hoggrp-1{
fsl,pins = <
MX6UL_PAD_UART1_RTS_B__GPIO1_IO19 0x17059 /* SD1 CD */
>
}
//设置gpio 节点:在设备树中 SD 卡节点下添加一个属性来描述 SD 卡的 CD 引脚就行了, SD卡驱动直接读取这个属性值就知道 SD 卡的 CD 引脚使用的是哪个GPIO 了。
&usdhc1 {
pinctrl-names = "default", "state_100mhz", "state_200mhz";
pinctrl-0 = <&pinctrl_usdhc1>;
pinctrl-1 = <&pinctrl_usdhc1_100mhz>;
pinctrl-2 = <&pinctrl_usdhc1_200mhz>;
/* pinctrl-3 = <&pinctrl_hog_1>; *///用来调用名为 "pinctrl_hog_1" 的引脚组配置。
cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;//用来指定一个GPIO控制引脚。描述了 SD 卡的 CD 引脚使用的哪个 IO。“GPIO_ACTIVE_LOW”表示低电平有效,如果改为“GPIO_ACTIVE_HIGH”就表示高电平有效。
keep-power-in-suspend;
enable-sdio-wakeup;
vmmc-supply = <®_sd1_vmmc>;
status = "okay";
};
// 根据上面这些信息, SD 卡驱动程序就可以使用 GPIO1_IO19 来检测 SD 卡的 CD 信号了
设备树-无pinctrl子系统
传统的配置 pin 的方式就是直接操作相应的寄存器,但是这种配置
方式比较繁琐、而且容易出问题(比如 pin 功能冲突)。
在根节点“/”下创建一个名为“alphaled”的子节点
alphaled {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-led";
status = "okay";
reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
0X0209C000 0X04 /* GPIO1_DR_BASE */
0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
};
设备树-pinctrl子系统
pinctrl 子系统主要工作内容如下:
①、获取设备树中 pin 信息。
②、根据获取到的 pin 信息来设置 pin 的复用功能
③、根据获取到的 pin 信息来设置 pin 的电气特性,比如上/下拉、速度、驱动能力等。
对于我们使用者来讲,只需要在设备树里面设置好某个 pin 的相关属性即可,其他的初始
化工作均由 pinctrl 子系统来完成, pinctrl 子系统源码目录为 drivers/pinctrl。
// SPI设备节点
&ecspi1{
fsl,spi-num-chipselects = <1>; // 表示只有一个设备
cs-gpios = <&gpio4 9 0>; // 片选信号为GPIO4_IO09
pinctrl-names = "defaults";// SPI设备所使用的IO名字
pinctrl-0 = <&pinctrl_ecspi1>;// 使用的IO对应的pinctrl节点
status = "okay"; // 状态属性
flash: m25p80@0{
// ECSP接口上接了一个m25p80设备,“0”表示 m25p80 的接到了 ECSPI 的通道 0上
#address-cells = <1>;
#size-cells = <1>;
//属性#address-cells 和#size-cells 都为 1,表示 reg 属性中起始地址占用一个字长
(cell),地址长度也占用一个字长(cell)。
compatible = "st, m25p32"; // 用于匹配设备驱动
spi-max-frequency = <20000000