Linux内核分析(七)----并发与竞态

简介: 原文:Linux内核分析(七)----并发与竞态Linux内核分析(七) 这两天家里的事好多,我们今天继续接着上一次的内容学习,上次我们完善了字符设备控制方法,并深入分析了系统调用的实质,今天我们主要来了解一下并发和竞态。
原文: Linux内核分析(七)----并发与竞态

Linux内核分析(七)

这两天家里的事好多,我们今天继续接着上一次的内容学习,上次我们完善了字符设备控制方法,并深入分析了系统调用的实质,今天我们主要来了解一下并发和竞态。

今天我们会分析到以下内容:

1.      并发和竞态简介

2.      竞态解决办法

3.      为我们的虚拟设备增加并发控制

 

在前几次博文我们已经实现了简单的字符设备,看似完美但我们忽视了一个很严重的问题,即并发问题,那么什么是并发,又如何解决并发呢,我们下面进行分析。

 

l  并发和竞态简介

1.       并发与竞态概念

1.        何为并发:并发是指多个执行单元同时、并行被执行。

2.        何为竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量,静态变量等)的访问容易发生竞态。

3.        我们虚拟设备的缺陷:对于我们前期的虚拟设备驱动个,假设一个执行单元A对其写入300个字符‘a’,而另一个执行单元B对其写入300个字符‘b’,第三个执行单元读取所有字符。如果AB顺序执行那么C读出的则不会出错,但如果AB并发执行,那结果则是我们不可料想的。

2.       竞态发生的情况

1.        对称多处理器(SMP)的多个CPUSMP是一种紧耦合、共享存储的系统模型,它的特点是多个CPU使用共同的系统总线,因此可以访问共同的外设和存储器。

2.        CPU内进程与抢占它的进程:2.6的内核支持抢占调度,一个进程在内核执行的时候可能被另一高优先级进程打断。

3.        中断(硬中断、软中断、tasklet、低半部)与进程之间:中断可以打断正在执行的进程,处理中断的程序和被打断的进程间也可能发生竞态。

3.       竞态的解决办法

解决竞态问题的途径是保证对共享资源的互斥访问。访问共享资源的代码区域称为临界区,临界区要互斥机制保护。Linux设备驱动中常见的互斥机制有以下方式:中断屏蔽、原子操作、自旋锁和信号量等。

l  竞态解决办法

上面我们已经分析了竞态产生的原因、发生的情况以及解决办法,下面我们对常见的解决办法一一分析。

1.       中断屏蔽

1.        基本概念:在单CPU中避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。由于linux的异步I/O、进程调度等很多内容都依靠中断,所以我们应该尽快的执行完临界区的代码,换句话就是临界区代码应该尽量少

2.        具体操作:linux内核提供了下面具体方法

 Local_irq_disable();//屏蔽中断

Local_irq_enable();//打开中断

Local_irq_save(flags);//禁止中断并保存当前cpu的中断位信息 

2.       原子操作

1.        基本概念:原子操作指在执行过程中不会被别的代码中断的操作。

2.        具体操作:linux内核提供了一系列的函数来实现内核中的原子操作,这些操作分为两类,一类是整型原子操作,另一类是位原子操作,其都依赖底层CPU的原子操作实现,所以这些函数与CPU架构有密切关系。

1)        整型原子操作

a)        设置原子变量的值

 atomic_t v = ATOMIC_INIT(0);//定义原子变量v并初始化为0

void atomic_set(atomic_t *v, int i);//设置原子变量值为i 

b)        获取原子变量的值

 atomic_read(atomic_t *v);//返回原子变量v的值 

c)        原子变量加、减操作

 void atomic_add(int i, atomic_t *v);//原子变量v增加i

void atomic_sub(int I, atomic_t *v);//原子变量v减少i 

d)        原子变量自增、自减

 void atomic_inc(atomic_t *v);//原子变量v自增1

void atomic_dec(atomic_t *v);//原子变量v自减1 

e)        操作并测试

 int atomic_inc_and_test(atomic_t *v);

int atomic_dec_and_test(atomic_t *v);  

 int atomic_sub_and_test(int i,atomic_t *v);

/*上述三个函数对原子变量v自增、自减和减操作(没有加)后测试其是否为0,如果为0返回true,否则返回false*/ 

f)         操作并返回

 int atomic_add_return(int i,atomic_t *v);

int atomic_sub_return(int i,atomic_t *v);  

 int atomic_inc_return(atomic_t *v);

int atomic_dec_return(atomic_t *v);

/*上述函数对原子变量v进行自增、自减、加、减操作,并返回新的值*/ 

2)        位原子操作

a)        设置位

 void set_bit(nr,void *addr);//设置addr地址的第nr位,即向该位写入1。 

b)        清除位

 void clear_bit(nr,void *addr);//清除addr地址的第nr位,即向该位写入0。 

c)        改变位

 void change_bit(nr,void *addr);//对addr地址的第nr取反 

d)        测试位

 int test_bit(nr,void *addr);//返回addr地址的第nr位 

e)        测试并操作位

 int test_and_set_bit(nr,void *addr);

int test_and_clear_bit(nr,void *addr); 

 int test_and_change_bit(nr,void *addr);

/*上述函数等同于执行test_bit后,再执行xxx_bit函数*/ 

3.       自旋锁

1.        基本概念:自旋锁是一种对临界资源进行互斥访问的手段。

2.        工作原理:为获得自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量,由于其为原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量,如果测试结果表明已经空闲,则程序获得这个自旋锁并继续执行,如果测试结果表明该锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗的说就是在“原地打转”。

3.        具体操作:linux内核中与自旋锁相关的操作主要有:

1)        定义自旋锁

 spinlock_t lock; 

2)        初始自旋锁

 spin_lock_init(lock); 

3)        获得自旋锁

 spin_lock(lock);//获得自旋锁lock

spin_trylock(lock);//尝试获取lock如果不能获得锁,返回假值,不在原地打转。 

4)        释放自旋锁

 spin_unlock(lock);//释放自旋锁  

为保证我们执行临界区代码的时候不被中断等影响我们的自旋锁又衍生了下面的内容

5)        自旋锁衍生

 spin_lock_irq() = spin_lock() + local_irq_disable()

spin_unlock_irq() = spin_unlock() + local_irq_enable()

spin_lock_irqsave() = spin_lock() + local_irq_save() 

 spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()

spin_lock_bh() = spin_lock() + local_bh_disable()

spin_unlock_bh() = spin_unlock() + local_bh_disable() 

4.        使用注意事项:

1)        自旋锁实质是忙等锁,因此在占用锁时间极短的情况下,使用锁才是合理的,反之则会影响系统性能。

2)        自旋锁可能导致系统死锁。

3)        自旋锁锁定期间不能调用可能引起进程调度的函数。

4.       读写自旋锁

1.        基本概念:为解决自旋锁中不能允许多个单元并发读的操作,衍生出了读写自旋锁,其不允许写操作并发,但允许读操作并发。

2.        具体操作:linux内核中与读写自旋锁相关的操作主要有:

1)        定义和初始化读写自旋锁

 rwlock_t my_rwlock = RW_LOCK_UNLOCKED;//静态初始化

rwlock_t my_rwlock;

rwlock_init(&my_rwlock);//动态初始化 

2)        读锁定

 read_lock();

read_lock_irqsave();

read_lock_irq();

read_lock_bh(); 

3)        读解锁

 read_unlock();

read_unlock_irqrestore();

read_unlock_irq();

read_unlock_bh(); 

4)        写锁定

 write_lock();

write_lock_irqsave();

write_lock_irq();

write_lock_bh();

write_trylock(); 

5)        写解锁

 write_unlock();

write_unlock_irqrestore();

write_unlock_irq();

write_unlock_bh(); 

5.       顺序锁

1.        基本概念:顺序锁是对读写锁的一种优化,如果使用顺序锁,读执行单元在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读不必等待写执行单元的完成,写执行单元也不需等待读执行单元完成在进行写操作。

2.        注意事项:顺序锁保护的共享资源不含有指针,因为在写执行单元可能使得指针失效,但读执行单元如果此时访问该指针,将导致oops

3.        具体操作:linux内核中与顺序锁相关的操作主要有:

1)        写执行单元获得顺序锁

 write_seqlock();

write_tryseqlock();

write_seqlock_irqsave();

write_seqlock_irq();

write_seqlock_bh(); 

2)        写执行单元释放顺序锁

 write_sequnlock();

write_sequnlock_irqrestore();

write_sequnlock_irq();

write_sequnlock_bh(); 

3)        读执行单元开始

 read_seqbegin();

read_seqbegin_irqsave();//local_irq_save + read_seqbegin 

4)        读执行单元重读

 read_seqretry ();

read_seqretry_irqrestore (); 

6.       RCU(读拷贝更新)

1.        基本概念:RCU可以看做是读写锁的高性能版本,相比读写锁,RCU的优点在于即允许多个读执行单元同时访问被保护数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。

2.        注意事项:RCU不能代替读写锁。

3.        具体操作:linux内核中与RCU相关的操作主要有:

1)        读锁定

 rcu_read_lock ();

rcu_read_lock_bh (); 

2)        读解锁

 rcu_read_unlock ();

rcu_read_unlock_bh (); 

3)        同步RCU

 synchronize_rcu ();//由RCU写执行单元调用

synchronize_sched();//可以保证中断处理函数处理完毕,不能保证软中断处理结束 

4)        挂接回调

 call_rcu ();

call_rcu_bh (); 

有关RCU的操作还有很多,大家可以参考网络。

7.       信号量

1.        基本概念:信号量用于保护临界区的常用方法与自旋锁类似,但不同的是当获取不到信号量时,进程不会原地打转而是进入休眠等待状态

2.        具体操作:linux内核中与信号量相关的操作主要有:

1)        定义信号量

 Struct semaphore sem; 

2)        初始化信号量

 void sema_init(struct semaphore *sem, int val);//初始化sem为val,当然还有系统定义的其他宏初始化,这里不列举 

3)        获得信号量

 void down(struct semaphore *sem);//获得信号量sem,其会导致睡眠,并不能被信号打断 

 int down_interruptible(struct semaphore *sem);//进入睡眠可以被信号打断 

 int down_trylock(struct semaphore *sem);//不会睡眠 

4)        释放信号量

   void up(struct semaphore *sem);//释放信号量,唤醒等待进程 

 

注:当信号量被初始为0时,其可以用于同步。

8.       Completion用于同步

1.        基本概念:linux中的同步机制。

2.        具体操作:linux内核中与Completion相关的操作主要有:

1)        定义Completion

 struct completion *my_completion; 

2)        初始化Completion

 void init_completion(struct completion *x); 

3)        等待Completion

 void wait_for_completion(struct completion *); 

4)        唤醒Completion

   void complete(struct completion *);//唤醒一个 

   void complete_all(struct completion *);//唤醒该Completion的所有执行单元 

9.       读写信号量

1.        基本概念:与自旋锁和读写自旋锁的关系类似

2.        具体操作:linux内核中与读写信号量相关的操作主要有:

1)        定义和初始化读写自旋锁

 struct rw_semaphore sem; 

 init_rwsem(&sem); 

2)        读信号量获取

 down_read ();

down_read_trylock(); 

3)        读信号量释放

 up_read (); 

4)        写信号量获取

 down_write ();

down_write_trylock (); 

5)        写信号量释放

 up_write(); 

10.  互斥体

1.        基本概念:用来实现互斥操作

2.        具体操作:linux内核中与互斥体相关的操作主要有:

1)        定义和初始化互斥体

struct mutex lock;

mutex_init(&lock); 

2)        获取互斥体

 void mutex_lock(struct mutex *lock);

int mutex_lock_interruptible(struct mutex *lock); 

 int mutex_lock_killable(struct mutex *lock); 

3)        释放互斥体

 void mutex_unlock(struct mutex *lock); 

 

上面我们介绍了linux内核中为了解决竞态所提供的方法,我们下面使用信号量为我们的虚拟设备增加并发控制。

l  为我们的虚拟设备增加并发控制

我们增加了并发控制后的代码如下,详细代码参考https://github.com/wrjvszq/myblongs

 

 1 struct mem_dev{
 2     struct cdev cdev;
 3     int mem[MEM_SIZE];//全局内存4k
 4     dev_t devno;
 5     struct semaphore sem;//并发控制所使用的信号量
 6 };
 7 static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos){
 8     unsigned long p = *ppos;
 9     unsigned int count = size;
10     int ret = 0;
11     int *pbase = filp -> private_data;
12 
13     if(p >= MEM_SIZE)
14         return 0;
15     if(count > MEM_SIZE - p)
16         count = MEM_SIZE - p;
17 
18     if(down_interruptible(&my_dev.sem))//获取信号量
19         return - ERESTARTSYS;
20 
21     if(copy_from_user(pbase + p,buf,count)){
22        ret = - EFAULT;
23     }else{
24         *ppos += count;
25         ret = count;
26     }
27 
28     up(&my_dev.sem);//释放信号量
29 
30     return ret;
31 }
32 static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos){
33     int * pbase = filp -> private_data;/*获取数据地址*/
34     unsigned long p = *ppos;/*读的偏移*/
35     unsigned int count = size;/*读数据的大小*/
36     int ret = 0;
37 
38     if(p >= MEM_SIZE)/*合法性判断*/
39         return 0;
40     if(count > MEM_SIZE - p)/*读取大小修正*/
41         count = MEM_SIZE - p;
42 
43     if(down_interruptible(&my_dev.sem))//获取信号量
44         return - ERESTARTSYS;
45 
46     if(copy_to_user(buf,pbase + p,size)){
47        ret = - EFAULT;
48     }else{
49         *ppos += count;
50         ret = count;
51     }
52     
53     up(&my_dev.sem);//释放信号量
54 
55     return ret;
56 } 

至此我们今天的工作完成,快过年了家里好多事,没有太多时间,还请大家见谅,提前祝大家新年快乐。

 

作者:wrjvsz 来源于:http://www.cnblogs.com/wrjvszq/,转载请注明出处。

 

目录
相关文章
|
20天前
|
存储 Linux 数据处理
探索Linux操作系统的内核与文件系统
本文深入探讨了Linux操作系统的核心组件,包括其独特的内核结构和灵活的文件系统。文章首先概述了Linux内核的主要功能和架构,接着详细分析了文件系统的工作原理以及它如何支持数据存储和检索。通过比较不同的文件系统类型,本文旨在为读者提供一个关于如何根据特定需求选择合适文件系统的参考框架。
|
24天前
|
安全 算法 网络协议
探索Linux操作系统的内核管理
【5月更文挑战第31天】本文将深入探讨Linux操作系统的内核管理机制,包括其设计原则、主要组件以及它们如何协同工作以提供高效的系统性能。通过分析Linux内核的关键特性和功能,我们将揭示这一开源操作系统如何在各种计算环境中保持其稳定性和灵活性。
|
25天前
|
机器学习/深度学习 人工智能 负载均衡
深度解析:Linux内核调度策略的演变与优化
【5月更文挑战第30天】 随着计算技术的不断进步,操作系统的性能调优成为了提升计算机系统效率的关键。在众多操作系统中,Linux因其开源和高度可定制性而备受青睐。本文将深入剖析Linux操作系统的内核调度策略,追溯其历史演变过程,并重点探讨近年来为适应多核处理器和实时性要求而产生的调度策略优化。通过分析比较不同的调度算法,如CFS(完全公平调度器)、实时调度类和批处理作业的调度需求,本文旨在为系统管理员和开发者提供对Linux调度机制深层次理解,同时指出未来可能的发展趋势。
|
1天前
|
Linux 数据处理 开发者
深入解析Linux中的paste命令:数据处理与分析的得力助手
`paste`命令在Linux中是数据处理的利器,它按列拼接多个文件内容,支持自定义分隔符和从标准输入读取。例如,合并`file1.txt`和`file2.txt`,使用`paste file1.txt file2.txt`,默认以制表符分隔;若要使用逗号分隔,可运行`paste -d ',' file1.txt file2.txt`。当文件行数不同时,较短文件后会填充空白行。结合管道符与其他命令使用,如`cat file1.txt | paste -s`,可按行合并内容。注意文件大小可能影响性能。
|
1天前
|
Linux 编译器 测试技术
探索Linux中的objcopy命令:数据处理与分析的得力助手
`objcopy`是GNU工具集中的实用程序,用于复制和转换二进制目标文件,如ELF到S-record。它支持格式转换、内容提取和修改,如移除调试信息。命令参数包括指定输入/输出格式和复制特定段。示例用途有:`objcopy -O binary input.elf output.bin`(ELF转二进制)和`objcopy -j .text input.elf output.o`(复制.text段)。使用时注意文件格式、备份原始文件并查阅文档。对于处理和分析二进制数据,`objcopy`是不可或缺的工具。
|
1天前
|
移动开发 数据挖掘 Linux
探索Linux命令之nl:数据处理与分析的得力助手
`nl`命令是Linux下用于为文本文件添加行号的工具,支持自定义格式和空行处理。它可以显示行首或行尾的行号,并能处理逻辑页。常用参数包括`-b`(控制空行行号)、`-n`(设定行号位置和是否补零)、`-w`(设定行号宽度)。示例用法如`nl -b a -n rz -w 3 filename.txt`。在处理大文件时需谨慎,并注意备份原始文件。nl是数据分析时的实用工具。
|
6天前
|
Linux
查看linux内核版本
在Linux中查看内核版本可使用`uname -r`、`cat /proc/version`、`lsb_release -a`(若安装LSB)、`/etc/*release`或`/etc/*version`文件、`dmesg | grep Linux`、`cat /sys/class/dmi/id/product_name`、`hostnamectl`、`kernrelease`(如果支持)、`rpm -q kernel`(RPM系统)和`dpkg -l linux-image-*`(Debian系统)。
15 4
|
7天前
|
安全 Linux 数据处理
探索Linux的kmod命令:管理内核模块的利器
`kmod`是Linux下管理内核模块的工具,用于加载、卸载和管理模块及其依赖。使用`kmod load`来加载模块,`kmod remove`卸载模块,`kmod list`查看已加载模块,`kmod alias`显示模块别名。注意需有root权限,且要考虑依赖关系和版本兼容性。最佳实践包括备份、查阅文档和使用额外的管理工具。
|
5天前
|
数据挖掘 Linux 数据处理
探索Linux下的Lua命令:轻量级脚本语言在数据处理和分析中的应用
**探索Linux上的Lua:轻量级脚本语言用于数据处理。Lua通过命令行解释器执行,适用于游戏开发、数据分析及自动化。特点包括小巧、高效、可扩展和动态类型。使用`lua`或`luajit`,配合-e、-l、-i参数执行脚本或互动模式。示例:执行`hello.lua`脚本打印"Hello, Lua!"。最佳实践涉及版本兼容、性能优化、使用C API、测试和文档编写。**
|
5天前
|
JSON 运维 安全
深入探索Linux的lsns命令:处理与分析Linux命名空间
`lsns`命令是Linux中用于查看命名空间信息的工具,帮助管理和隔离系统资源。它显示命名空间的状态、类型、进程和挂载点,适用于性能优化、故障排查。命令特点包括丰富的参数选项(如 `-t`、`-p`、`-n`),清晰的表格输出和JSON格式支持。示例:列出所有命名空间用`lsns`,列出网络命名空间用`lsns -t net`。使用时注意权限,结合其他工具,并考虑版本兼容性。