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(" 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
六、总结
这节介绍了内核中并发产生的原因,并介绍了在单处理器的情况下如何保护内核中的共享资源同时实现内核同步。
有一个不足,就是写出来的代码很难去验证,这是因为,即使并发是存在,但是很难保证一定会在临界区发生,毕竟临界区代码不长。
还有一个地方我觉得没有讲清楚的,上面介绍了的是防止并发的方法,不一定需要全用。
如单处理器非抢占内核,如果你知道中断代码中根本没有访问另一个进程临界区的资源,你的进程完全可以不关中断。
同样的,单处理器抢占内核的情况下。如果只有中断会访问到临界区的资源,那你完全可以不关抢占(但是这样的情况好像很难说,同时打开两个相同的程序就会有可能发生临界区并发访问)。
所以说,代码用在需要它的地方,不要随便加。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
源代码: 4th_mutex_4.rar