本文转载:http://blog.chinaunix.net/uid-25014876-id-67005.html
linux设备驱动总结(4):4.单处理器下的竞态并发
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
经过以上几节的铺垫,我们终于要集中精力了。由于核心过程的调度和中断(中断尚未讨论,但这里将大致讨论),它们将进入核心共享核心资源。因此,只要你不小心,你自己过程中的资源就会在不经意间被其他过程修改。本节将介绍并讨论如何解决它。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
什么是并发?
所谓并发,就是多个过程同时并行执行。在单处理器的情况下,并发只是宏观的。用户会觉得多个程序是一起执行的。事实上,只有多个过程轮流占用处理器,只有在多处理器的情况下才能同时真正执行。
然而,无论是单处理器还是多处理器,核心中的并发性都会导致共享资源的并发访问。例如,有两个相同代码的过程并发执行,它们都需要修改存在和核心中的数据data。
情况一:无错:
上面的例子是单处理器的内核调度AB过程分别在处理器上运行,A运行后再运行B,情况很理想,没有出错。
情况二:错了。
同样是单处理器,但是有问题。过程A执行到一半,内核调度过程B执行,过程B执行后返回执行A。仔细想想,你会发现有问题。过程B等于白干!我最后保存的只是执行过程B前的data。
以上只是想解释一下,在并发执行的情况下,我们无法预测过程何时会被调度,一些核心共享资源应该得到相应的保护。没什么大不了的。如果系统崩溃,那就太糟糕了。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
二、临界区和竞争条件是什么?
临界区是访问和操作共享数据的代码段。正如我之前所说,并发访问共享资源的过程是不安全的,因为它访问了临界区域的数据。如果两个过程同时离开临界区域,资源将被抢劫,这被称为竞争条件。避免并发和防止竞争条件同步,这将是下一个重点讨论。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
三、什么会导致内核并发?
请注意,该过程在用户空间和核心空间中并发执行,但这里主要讨论核心的并发性。系统编程中应了解用户空间的并发性,如多过程共享文件。
在以下情况下,内核导致并发执行:
1)中断:中断可随时发生。一旦核心被中断,它将放下手头的工作,优先考虑中断。如果中断代码修改了以前操作过程中的共享资源,则会出现bug。
2)内核抢占:前一节已经介绍,在支持内核抢占的情况下,正在实施的过程可能随时被抢占。
3)睡眠:当在核心执行过程中睡眠时,唤醒调度程序,调度新过程执行。
4)多处理器:多处理器可以同时执行多个过程。这是同时真正执行的。
既然你知道在什么情况下会导致并发,在编写代码时,你应该考虑保护临界区域。在临界区域,可以避免并发,从而保护共享资源,即核心同步。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
四、单处理器不支持内核抢占
我现在使用的内核版本2.6.29不支持内核抢占。分析两种情况。
情况一,两个过程:
在单处理器不支持抢占的情况下,运行在内核的两个内核线程不会并发。
情况二,中断与进程之间:
中断上下文与普通内核线程之间的并发性。当内核线程正在执行时,可能会随时中断。因此,在临界区域的代码可以通过关闭中断来避免并发。
首先写一个程序,看看中断可以在没有中断的情况下中断正在执行的内核线程。
例子源代码为:4th_mutex_4/1st
首先,看看如何写驱动代码:
/*1st/test.c*/
41 ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
42 {
43 int ret;
44 unsigned long flag = 0;
45
46 for(; ;)
47 {
48 printk("[%s]task pid[%d], context [%s]\n", __FUNCTION__, current->pid, current->comm);
49 mdelay(2000);
50 }
51
52 return count - ret;
53 }
首先,这个驱动程序的功能与我所说的完全不同。在这里,我只是想实现内核线程在内核中的死循环,除了中断test_read应用程序不能冒泡函数的执行。
驱动函数出来了,接下来要看应用代码是如何实现的:
/*app/app.c*/
8 int main(void)
9 {
10 for(;;)
11 {
12 printf("runing\n");
13 sleep(2);
14 }
15
16 return 0;
17 }
这个应用程序不需要进入核心操作,他只是每两秒打印一句话。
看看另一个应用程序:
/*app/app_read.c*/
5 int main(void)
6 {
7 char buf[20];
8 int fd;
9 fd = open("/dev/test", O_RDWR);
10 if(fd < 0)
11 {
12 perror("open");
13 return -1;
14 }
15 printf("<app_read>pid[%d]\n", getpid());
16 read(fd, buf, 10);
17
18 return 0;
19 }
当这个程序调用时read当系统调用时,内核将被调用test_read,此时,进程就会在内核中陷入循环。
另外,还需要注册中断,当我按下开发板上的按钮时,就会打印出来。key down.因为中断的实现还没有介绍,这里就不解释代码了。(这里的中断是按照我的开发板写的,按钮对应EINT1.所以加载后可以无效)
看实验效果:
[root: 1st]# cd irq/
[root: irq]# insmod irq.ko //注册中断,其实也是记载模块
hello irq
[root: irq]# cd ../
[root: 1st]# insmod test.ko //加载模块
alloc major[253], minor[0]
hello kernel
[root: 1st]# mknod /dev/test c 253 0
[root: 1st]# cd app/
[root: app]# ./app& //后台运行app
runing //app快乐地运行
[root: app]#runing
runing
runing
runing
runing
runing
runing
runing
[root: app]# ./app_read //运行app_read
[test_open]
pid[404]
[test_read]task pid[404], context [app_read] //过程在内核中循环,不支持内核
[test_read]task pid[404], context [app_read] //在抢占的情况下,即使在睡眠的过程中,应用空间
[test_read]task pid[404], context [app_read] //进程app无法获得调度。因为过程。
[test_read]task pid[404], context app_read] //没有返回用户空间。
[test_read]task pid[404], context [app_read]
[test_read]task pid[404], context [app_read]
key down //但是,当我按下按键,中断产生,内核
[test_read]task pid[404], context [app_read] //执行中断处理函数。
key down
key down
key down
key down
[test_read]task pid[404], context [app_read] //用户空间的进程不能打印。
[test_read]task pid[404], context [app_read]
[test_read]task pid[404], context [app_read]
上面的例子说明了两个情况:
1)只要进程运行在内核上下文,就不会被内核抢占去执行 别的进程。
2)但是,中断会打断正在内核运行的进程,出现并发。
当然,我的test_read函数只是打印一句话,如果我的函数正在修改共享资源,这时是不能允许中断产生并且修改正在被使用的共享资源。所以,为了避免并发和防止竞争条件,只需要把中断关闭就可以了。
接下来讲一下关闭中断的方法:
方法一:
local_irq_disable(); //关中断
/*执行临界区代码*/
local_irq_enable(); //开中断
但是,这种方法有缺陷,如果内核本来就是关闭中断的,上面的代码却在最后把中断打开了,这是多不合理的做法。所以有了下面的函数:
unsigned long flag;
local_irq_save(flag); //在关中断前,先报存原来的中断状态
/*执行临界区代码*/
local_irq_restore(flag); //开启中断,然后还原原来的中断状态
通过上面的代码,就可以解决上面所说的缺陷。还要注意的是,关中断的时间不能太长。
现在改进一下原来的代码,在访问临界资源时关闭中断:
/*4th_mutex_4/2nd/test.c*/
41 ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
42 {
43 int ret;
44 unsigned long flag = 0;
45
46 for(; ;)
47 {
48 local_irq_save(flag);
49 //假设这是临界区
50 printk("[%s]task pid[%d], context [%s]\n", __FUNCTION__, current->pid, current->comm);
51 local_irq_restore(flag);
52 mdelay(2000);
53 }
54
55 return count - ret;
56 }
添加了这两句代码,在访问临界区时就不会被中断打断了。上面的代码我就不验证了,也验证不出效果,因为临界区太小了,通过我按键产生的中断进入临界区的概率自然就小,只要大家知道通过关中断就能防止中断处理函数打断原来的进程就行了。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
五、单处理器又支持内核抢占的情况下
同样分析上面的两种情况:
情况一,两个进程之间:
在单处理器支持抢占的情况下,运行在内核的两个内核线程,会产生并发。访问临界区时需要关抢占。
情况二,中断与进程之间:
中断上下文与普通内核线程之间,会产生并发。在内核线程正在执行时,随时会有可能被中断打断。所以,在临界区的代码,可以通过关闭中断来避免并发。
注意:在我的开发板2.6.29的内核是不支持内核抢占的,为了能够支持内核抢占,需要打开以下选项并重新编译内核。
1、General setup
Prompt for development and/or incomplete code/drivers /*选择使用开发中的驱动代码*/
2、Kernel Features
Preemptible Kernel /*选择开启抢占式内核*/
重现编译并运行1st目录的代码,你会发现跟原来不一样的地方:
[root: 1st]# insmod test.ko //加载模块
alloc major[253], minor[0]
hello kernel
[root: 1st]# mknod /dev/test c 253 0
[root: 1st]# cd irq/
[root: irq]# insmod irq.ko //加载中断
hello irq
[root: irq]# cd ../app/
[root: app]# ./app& //后天运行app
[root: app]# runing //app欢快地独自运行
runing
runing
runing
[root: app]# ./app_read //再运行app_read
runing
[test_open]
pid[401]
[test_read]task pid[401], context [app_read] //在支持内核抢占下,app和app_read交替运行
runing
[test_read]task pid[401], context [app_read]
runing
[test_read]task pid[401], context [app_read]
key down //当我按下按键后,中断马上处理中断函数
key down
key down
runing
[test_read]task pid[401], context [app_read]
可能你在前一节我介绍内核抢占的时候还是不明白内核抢占是怎么一回事,但看到同样的程序(目录1st),在支持和不支持内核抢占的内核下运行的不同结果,想该明白了吧。不支持内核抢占的内核是霸道的,只要进程还运行在内核上下文,除了中断就没其他人能够打断。
言归正传,为了避免并发,在单处理器支持抢占的情况下,需要防两个情况:
情况一:内核线程之间并发访问临界区。
这个解决办法很简单,既然这种情况是因为内核支持内核抢占引起的,那我访问临界区时把内核抢占关掉就好了!包含头文件 。
preempt_disable(); 关抢占
... 临界区代码
preempt_enable(); 开抢占
这两个函数的实现原理也很简单,有这样一个计数器,当执行preempt_disable()时计数器加一,当执行preempt_enable()时计数器减一,只有当计数器的值为0时,内核才可以抢占。
只要把代码稍作修改,运行的结果就和非抢占时运行1st的代码一样:
/*4th_mutex_4/3th/test.c*/
41 ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
42 {
43 int ret;
44 unsigned long flag = 0;
45
46 preempt_disable(); //在死循环前关掉抢占。
47 for(; ;)
48 {
49 printk("[%s]task pid[%d], context [%s]\n", __FUNCTION__, current->pid , current->comm);
50 mdelay(2000);
51 }
52 preempt_enable();
53
54 return count - ret;
55 }
注意:上面的代码是完全不合理的。我只是想说明内核抢占的模式下怎么把抢占关掉。关掉抢占是为了保护临界区的共享数据,而上面的代码会导致系统陷入死循环。
情况二:中断程序打断正在运行的内核线程。
同样的,我把中断关掉就可以了。结果代码编程这样子:
preempt_disable();
unsigned long flag;
local_irq_save(flag);
临近区代码
local_irq_restore(flag);
preempt_enable();
所以,为了保护临界区的共享数据,代码应该改成这样。
/*4th_mutex_4/4th/test.c*/
41 ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
42 {
43 int ret;
44 unsigned long flag = 0;
45
46 for(; ;)
47 {
48 preempt_disable();
49 local_irq_save(flag);
50 printk("[%s]task pid[%d], context [%s]\n", __FUNCTION__, current->pid , current->comm); //假设这是临界区的代码。。。
51 local_irq_restore(flag);
52 preempt_enable();
53 mdelay(2000);
54 }
55
56 return count - ret;
57 }
大功告成!这个也不验证了。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
六、总结
p { margin-bottom: 0.21cm; }code.cjk { font-family: "DejaVu Sans",monospace; }code.ctl { font-family: "DejaVu Sans Mono",monospace; }
这节介绍了内核中并发产生的原因,并介绍了在单处理器的情况下如何保护内核中的共享资源同时实现内核同步。
有一个不足,就是写出来的代码很难去验证,这是因为,即使并发是存在,但是很难保证一定会在临界区发生,毕竟临界区代码不长。
还有一个地方我觉得没有讲清楚的,上面介绍了的是防止并发的方法,不一定需要全用。
如单处理器非抢占内核,如果你知道中断代码中根本没有访问另一个进程临界区的资源,你的进程完全可以不关中断。
同样的,单处理器抢占内核的情况下。如果只有中断会访问到临界区的资源,那你完全可以不关抢占(但是这样的情况好像很难说,同时打开两个相同的程序就会有可能发生临界区并发访问)。
所以说,代码用在需要它的地方,不要随便加。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx