Linux 并发与竞争

news2024/11/27 17:39:54

一、并发与竞争

1、并发

Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可 能会相互覆盖这段内存中的数据,造成内存数据混乱。

  • 多线程并发访问, Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
  • 抢占式并发访问,从 2.6 版本内核开始, Linux 内核支持抢占,也就是说调度程序可以 在任意时刻抢占正在运行的线程,从而运行其他的线程。
  • 中断程序并发访问。
  • SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并 发访问。

2.竞争

  • 并发访问带来的问题就是竞争,对于共享数据段必须保证一次只有一个线程访问。
  • 如果多个线程同时操作临界区就表示存在竞争,我们在编写驱动的时候一定要注意避免并发和防止竞争访问。很多 Linux驱动初学者往往不注意这一点,在驱动程序中埋下了隐患,这类问题往往又很不容易查找,导致驱动调试难度加大、费时费力。

二、原子操作

所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位。因此这里的原子实际是使用了物理学里的物质微粒的概念。

原子整形操作 API 函数

typedef struct {
     int counter;
 } atomic_t;

如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,如下所示:

atomic_t a; //定义 a

也可以在定义原子变量的时候给原子变量赋初值,如下所示:

atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0

可以通过宏 ATOMIC_INIT 向原子变量赋初值。

  • 原子变量有了,接下来就是对原子变量进行操作,比如读、写、增加、减少等等,Linux 内 核提供了大量的原子操作 API 函数
函数描述
ATOMIC_INIT(int i)定义原子变量的时候对其初始化。
int atomic_read(atomic_t*v)读取 v的值,并且返回
void atomic_set(atomic_t *v, int i)向 v写入 i值。
void atomic_add(int i, atomic_t *v)给 v加上 i值。
void atomic_sub(int i, atomic_t *v)从 v减去 i值。
void atomic_inc(atomic_t *v)给 v加 1,也就是自增。
void atomic_dec(atomic_t *v)从 v减 1,也就是自减 。
int atomic_dec_return(atomic_t *v)从 v减 1,并且返回v的值 。
int atomic_inc_return(atomic_t *v)给 v加 1,并且返回 v的值。
int atomic_sub_and_test(int i, atomic_t *v)从 v减 i,如果结果为0就返回真,否则就返回假
int atomic_dec_and_test(atomic_t *v)从 v减 1,如果结果为0就返回真,否则就返回假
int atomic_inc_and_test(atomic_t *v)给 v加 1,如果结果为0就返回真,否则就返回假
int atomic_add_negative(int i, atomic_t *v)

给 v加 i,如果结果为负就返回真,否则返回假

注:64位的整型原子操作只是将“atomic_”前缀换成“atomic64_”,将int换成long long

原子位操作 API 函数 

位操作也是很常用的操作,Linux 内核也提供了一系列的原子位操作 API 函数,只不过原 子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作。

函数描述
void set_bit(int nr, void *p)将p地址的nr位置1
void clear_bit(int nr,void *p)将p地址的nr位清零
void change_bit(int nr, void *p)将p地址的nr位反转
int test_bit(int nr, void *p)获取p地址的nr位的值
int test_and_set_bit(int nr, void *p)将p地址的nr位置1,并且返回nr位原来的值
int test_and_clear_bit(int nr, void *p)将p地址的nr位清0,并且返回nr位原来的值
int test_and_change_bit(int nr, void *p)将p地址的nr位翻转,并且返回nr位原来的值

 原子操作例程

/* 定义原子变量,初值为1*/
static atomic_t xxx_available = ATOMIC_INIT(1); 
static int xxx_open(struct inode *inode, struct file *filp)
{
 ...
 /* 通过判断原子变量的值来检查LED有没有被别的应用使用 */
 if (!atomic_dec_and_test(&xxx_available)) {
 /*小于0的话就加1,使其原子变量等于0*/
 atomic_inc(&xxx_available);
 /* LED被使用,返回忙*/
 return - EBUSY; 
 }
...
/* 成功 */
 return 0;
static int xxx_release(struct inode *inode, struct file *filp)
{
 /* 关闭驱动文件的时候释放原子变量 */
 atomic_inc(&xxx_available); 
 return 0;
}

三、自旋锁

1、自旋锁简介

原子操作只能对整型变量或者位进行保护。但是在实际中不仅仅只有只有整型变量和位这样简单的临界区。举个简单的例子,设备结构体变量就不是整型变量,我们对于结构体中的成员变量操作也要保证原子性,在线程A对结构体变量使用期间,应该禁止其他的线程来访问此结构体变量,在这里可以用到自旋锁。

自旋锁是一种用于保护多线程共享资源的锁,与一般互斥锁不同之处在于自旋锁尝试获取锁时以忙等待的形式不断的循环检查锁是否可用。

在多cpu的环境下,对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能。

在使用自旋锁之前,肯定要先定义一个自旋锁变量,定义方法如下所示:

spinlock_t lock; //定义自旋锁

定义好自旋锁变量以后就可以使用相应的 API 函数来操作自旋锁。

自旋锁API函数

函数描述
DEFINE_SPINLOCK(spinlock_t lock)定义并初始化一个自旋变量
int spin_lock_init(spinlock_t *lock)初始化自旋锁
void spin_lock(spinlock_t *lock)获取指定的自旋锁,也叫加锁
void spin_unlock(spinlock_t *lock)释放指定的自旋锁。
int spin_trylock(spinlock_t *lock)尝试获取指定的锁,如果没有获取到,返回0
int spin_is_locked(spinlock_t *lock)检查指定的自旋锁是否被获取,如果没有被获取返回非0,否则返回0.

上述自旋锁的API函数适用于SMP(多核)或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API函数,否则会导致死锁现象的发生。

 缺陷:

自旋锁一直处于自旋状态,这样会浪费处理器时间,所有自旋锁的持有时间不能太长,要及时释放掉。

注意事项:

临界区内不要有阻塞(sleep)和中断

获取锁之前关闭本地中断。

使用 spin_lock_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际 上内核很庞大,运行也是“千变万化”,我们是很难确定某个时刻的中断状态,因此不推荐使用 spin_lock_irq/spin_unlock_irq。建议使用 spin_lock_irqsave/spin_unlock_irqrestore,因为这一组函 数会保存中断状态,在释放锁的时候会恢复中断状态。一般在线程中使用 spin_lock_irqsave/ spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock,

DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */
 
 /* 线程 A */
 void functionA (){
 unsigned long flags; /* 中断状态 */
 spin_lock_irqsave(&lock, flags) /* 获取锁 */
 /* 临界区 */
 spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
 }

 /* 中断服务函数 */
 void irq() {
 spin_lock(&lock) /* 获取锁 */
 /* 临界区 */
 spin_unlock(&lock) /* 释放锁 */
 }

下半部(BH)也会竞争共享资源,使用的API

 四、信号量

相比于自旋锁,信号量可以使线程进入休眠状态,使用信号量会提高处理器的使用效率,毕竟不用一直等待。信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。

信号量的特点:

  1. 因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合
  2. 因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
  3. 如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。

信号量 API 函数

//Linux 内核使用 semaphore 结构体表示信号量,结构体内容如下
struct semaphore {
 raw_spinlock_t lock;
 unsigned int count;
 struct list_head wait_list;
};

 信号量的使用如下所示:

struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */

信号量实战范例

//头文件
#include <linux/semaphore.h>

//设备结构体
struct gpioled_dev{
	dev_t devid;		
	struct semaphore sem;	/* 信号量 */
};

static int led_open(struct inode *inode, struct file *filp)
{
	/* 获取信号量 */
	if (down_interruptible(&gpioled.sem)) { /* 获取信号量,进入休眠状态的进程可以被信号打断 */
		return -ERESTARTSYS;
	}
#if 0
	down(&gpioled.sem);		/* 不能被信号打断 */
#endif

	return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
	up(&dev->sem);		/* 释放信号量,信号量值加1 */
	return 0;
}
static int __init led_init(void)
{
	/* 初始化信号量 */
	sema_init(&gpioled.sem, 1);
	
}

五、互斥体

Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申请互斥体。在我们编写 Linux 驱动的时候遇到需要互斥访问的地方建议使用 mutex。

Linux 内核使用 mutex 结构体表示互斥体:

struct mutex {
 /* 1: unlocked, 0: locked, negative: locked, possible waiters */
 atomic_t count;
 spinlock_t wait_lock;
};

在使用 mutex 之前要先定义一个 mutex 变量。在使用 mutex 的时候要注意如下几点:

①、mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。

②、和信号量一样,mutex 保护的临界区可以调用引起阻塞的 API 函数。

③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并 且 mutex 不能递归上锁和解锁

互斥体 API 函数

 互斥体的使用如下所示:

 struct mutex lock; /* 定义一个互斥体 */
 mutex_init(&lock); /* 初始化互斥体 */

 mutex_lock(&lock); /* 上锁 */
 /* 临界区 */
 mutex_unlock(&lock); /* 解锁 */

互斥体实战范例

struct gpioled_dev{
	dev_t devid;		
	struct mutex lock;		/* 互斥体 */
};

static int led_open(struct inode *inode, struct file *filp)
{
	/* 获取互斥体,可以被信号打断 */
	if (mutex_lock_interruptible(&gpioled.lock)) {
		return -ERESTARTSYS;
	}
#if 0
	mutex_lock(&gpioled.lock);	/* 不能被信号打断 */
#endif
	return 0;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/524739.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

命令firewalld和firewall-cmd用法

firewalld命令跟firewall-cmd 1.启动firewalld服务 systemctl start firewalld.service2.关闭firewalld服务 systemctl stop firewalld.service3.重启firewalld服务 systemctl restart firewalld.service4.查看firewalld状态 systemctl status firewalld.service5.开机自启…

接口测试怎么做?全网最详细从接口测试到接口自动化详解,看这篇就够了...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 抛出一个问题&…

孙鑫VC++第三章 2.基于MFC的程序框架剖析

目录 1. MFC向导生成类 2. 框架流程 2.1 WinMain 2.2 全局对象&#xff1a;theApp 2.3 AfxWinMain函数 1.AfxWinMain&#xff1a; 2.AfxGetThread函数&#xff08;thrdcore.cpp&#xff09;&#xff1a; 3.AfxGetApp是一个全局函数&#xff0c;定义于&#xff08;afxwin1…

原型/原型链/构造函数/类

认识构造函数 为什么有构造函数 因为一般的创建对象的方式一次只能创建一个对象, 利用工厂模式创建的对象&#xff0c;对象的类型都是Object类型 什么是构造函数 构造函数也称之为构造器&#xff08;constructor&#xff09;&#xff0c;通常是我们在创建对象时会调用的函数…

Uni-app项目应用总结(一)

目录 一.新建uniapp项目 第一步&#xff1a;下载HBuilder 第二步:创建uni-app项目 第三步&#xff1a;运行uni-app 二.uni-app组件使用 三.uni-app路由跳转  1.页面路由配置    (1)在pages.json中配置页面路由    (2)在pages.json中配置底部导航栏 2.路由跳转方法…

【输配电路 DZY-104端子排中间继电器 接通、信号转换 JOSEF约瑟】

DZY-104端子排中间继电器品牌:JOSEF约瑟型号:DZY-104名称:端子排式中间继电器触点容量:5A/250V功率消耗:≤1.5W/≤3W/≤7W/≤3VA/≤7VA/≥5W绝缘电阻:≥10MΩ 系列型号&#xff1a; DZY-101端子排中间继电器&#xff1b; DZY-104端子排中间继电器&#xff1b; DZY-105端子排…

华南农业大学|图像处理与分析技术综合测试|题目解答:求芒果单层坏损率

设计任务 对于一幅芒果果实内部的 CT 断层图像&#xff0c;试采用图像处理与分析技术&#xff0c;设计适当的算法和程序&#xff0c;首先分割出其中的坏损区域&#xff0c;然后计算其像素面积占整个果肉区域的百分比&#xff08;单层坏损率&#xff09;。请按统一要求写出算法…

nuc980 uboot 2017.11 移植:env 保存位置选择问题

开发环境 Win10 64位 ubuntu 20.04 虚拟机 VMware Workstation 16 Pro 开发板&#xff1a;NK-980IOT&#xff08;NUC980DK61Y&#xff09; gcc 交叉编译工具链&#xff1a; ARM 官方 gcc version 11.2.1 20220111 NUC980 uboot 版本 &#xff1a;尝试移植到 u-boot-2017.1…

科普 “平均工资又涨了”

周四晚上做了一个图&#xff0c;发了一则朋友圈&#xff0c;科普了一下为什么平均工资一直在涨&#xff1a; 曲线是 drawio 画的&#xff0c;不是类似 geogebra 画的精确数学函数&#xff0c;误差比较大&#xff0c;但大概就是这个意思。 收入应该是无标度分形的幂律分布&am…

孙鑫VC++第三章 4.窗口类、窗口类对象与窗口三者之间关系

目录 1. 创建CWnd 2. WinMain 3. 创建CButton 1. 创建CWnd 模拟CWnd类的封装过程。在解决方案ch04下添加一个新的空项目&#xff0c;项目名称为&#xff1a;WinMain&#xff0c;在项目创建完成后&#xff0c;将WinMain项目设为启动项目。 接下来在WinMain项目中添加一个名…

【C++ 学习 ④】- 类和对象(下)

目录 一、初始化列表 1.1 - 定义 1.2 - 使用初始化列表的原因 1.3 - 成员变量的初始化顺序 二、静态成员 2.1 - 静态成员变量 2.2 - 静态成员函数 三、友元 3.1 - 友元函数 3.2 - 友元类 四、内部类 五、匿名对象 5.1 - 匿名对象的特性 5.2 - 匿名对象的使用场景…

3.View的绘制流程

View是在什么时候显示在屏幕上面的?(如:MainActivity的布局文件activity_main.xml) setContentView最终的结果是将解析的xml文件中的View添加到DecorView中. 那么这个DecorView是什么时候添加到Window(PhoneWindow)的呢? DecorView是在ActivityThread.java的handleResumeA…

2-Zookeeper单机版安装

2-Zookeeper单机版安装 本文介绍的是 Linux 系统下 Zookeeper 安装方式 ① 下载 进入官网 https://zookeeper.apache.org/ 点击下载按钮 进入下载页 https://zookeeper.apache.org/releases.html 后选择 最新的稳定版本&#xff0c;如下&#xff1a; 3.7.1 为最新的稳定版本…

号称分割一切的图片分割模型开源了——Segment Anything Meta SAM

头条号:人工智能研究所 微信号:启示AI科技 微信小程序:AI人工智能工具 以前,要解决任何类型的分割问题,有两类方法。第一种是交互式分割,允许分割任何类别的对象,但需要人通过迭代细化掩码来指导。第二种,自动分割,允许分割提前定义的特定对象类别(例如,猫或椅子),…

【计算机系统】指令

leaq指令 一元指令 二元指令 例子 指令addq 指令subq 指令incq 指令subq 移位指令 移位指令用途 特殊运算指令

LitCTF2023 郑州轻工业大学首届网络安全赛 WP 部分

LitCTF2023 郑州轻工业大学首届网络安全赛 WP 部分 前言&#xff1a;Web&#xff1a;我Flag呢&#xff1f;导弹迷踪&#xff1a;Follow me and hack me&#xff1a;PHP是世界上最好的语言&#xff01;&#xff01;作业管理系统&#xff1a;Vim yyds&#xff1a;Ping&#xff1a…

Java基础-面向对象总结(2)

这篇文章主要讲解 Java中的 变量方法代码块访问修饰限定符Java 是值传递&#xff0c;还是引用传递&#xff1f;类和对象的生命周期..... 希望给您带来帮助 目录 变量 成员变量与局部变量的区别 静态变量和实例变量的区别&#xff1f;静态方法、实例方法呢&#xff1f; 可以…

数据分析06——Pandas中的数据抽取

1、前言&#xff1a; 在Pandas中进行数据抽取主要有两种方法&#xff0c;一种是loc方法&#xff0c;一种是iloc方法&#xff1b;在获取数据时可以获取的数据有三种形式&#xff0c;一种是Series类型&#xff0c;一种是DataFrame类型&#xff0c;还有一种是直接获取数据值&…

Nginx make报错处理

文章目录 make报错&#xff1a;fatal error:sys/sysctl.h:No such file or directory问题处理 make 报错&#xff1a;error: this statement may fall through [-Werrorimplicit-fallthrough]问题处理 make报错&#xff1a;error: struct crypt_data has no member named curre…