在Linux系统中,进程是可被调度的实体。虽然当线程被发明出来之后,进程更多的是被当做资源的实体,而线程作为可被调度的执行单元被操作系统调度,但是,Linux因为历史等原因,使得它调度的实体仍然是进程。
进程之间因为可以“并发”,所以他们之间的同步要通过某种方式的通信实现,这也就是进程间通信(IPC: Inter-Process Communication)。在用户空间,我们可以通过信号量,消息队列,共享内存和UNIX域套接字等多种方式实现。但是在内核空间,因为其对所有资源都是可见的,所以需要考虑的关系自然也就多了,事情一多,一切也都变得复杂起来,稍有不慎,就可能造成无法想象的后果。呵呵,似乎有些危言耸听,不过当心一点儿总归是没有坏处的。
用户空间的进程间通信想必大家都很清楚,因为是内核编程,所以接下来着重讲一下通过系统调用进入内核空间的进程和内核进程(有的系统称其为内核线程,可能线程更能体现他们之间共享内核数据结构的这个事实,不过前者在内核空间的权能似乎和后者没有什么差别,所以,统称其为进程也没有什么不妥。以后内核线程和内核进程也是互换使用的,请不要惊讶,咬文嚼字不是什么好习惯。)之间的进程间通信需要注意的事项。
内核线程和中断:在前面的文章,我们已经知道无论是软中断,或者是硬件中断,其优先级都是比内核线程高,所以,如果当前的进程没有屏蔽相应中断(软中断和硬中断),那么总是可以被中断的。有的文章中把这个过程也叫做抢占(Preempt),事实上,这样的称呼是欠妥的,因为只有进程才是可以调度的,而抢占是针对调度的,所以,我们还是叫他中断(Interrupt)吧!
如果进程和中断共享某个数据结构,为了保持数据的完整性,请在访问数据的时候,关闭相应中断。抢占式内核:2.4及以前的内核都是不可抢占的,那么不可抢占是什么意思呢?中国的语言就是巧妙,基本上都可以顾名思义,且一般八九不离十。抢占:就是抢过来,并且占有。对于进程而言,抢的当然就是CPU资源了。不可抢占的内核,调度的时机是非常少的,尤其是当其进入内核空间,也就是穿上马甲之后,还真不能小瞧他小样的,如果他不自愿让贤,还真不能拉他下马。非抢占式内核的调度时机有:
- 进程自己调用schedule()函数,将CPU让出去,有点儿类似中国古代的掸让制度,也和人共享的美德相一致,好东西就应该和大家共享嘛,不是说;“独乐乐,不如众乐乐“。
- 进程从系统调用或者是中断返回用户空间的时候,如果这个时候,当前进程需要重新被调度,那么将发生调度。请注意,是返回到用户空间,这也是区分可抢占和非可抢占内核的关键所在。试想,一个进程通过系统调用进入了内核空间,此时如果产生了一个中断,假如是接收到了一个键盘击键的中断,那么在中断处理函数中,等待这个中断的进程将被唤醒,当前进程被标识为需要重新调度,当中断返回时,因为其是从内核空间进入中断的,调度不会马上执行,而必须等到此进程从系统调用返回的时候,或者是他主动调用了schedule函数,才会发生调度,即使是被唤醒的进程比当前进程更具有执行权,他也不能不等待。事情还不是很糟糕,只是系统的响应不是很快,最糟糕的是,一个比较马虎的程序员如果在进程的内核空间形成了一个死循环,那么这个系统的其它进程将永远再也得不到CPU的使用权,系统就是马上挂机,当然,即使是存在抢占的,这中情形也是绝对不允许出现的。
在情况二中,我们得知并不是到了调度点就能真的发生调度,只有同时当前进程的需要被重新调度的标志设置了,调度才会发生。那么这个标志什么时候会被设置呢?有以下两个地点:
- 在现代的分时操作系统中,每个进程都被分配一个时间片,当时钟中断发生时,将更新这个时间片的值,也就是将时间片的值减一,如果发现时间片的值为零,则表示当前进程的时间片用完了,需要被重新调度,设置当前进程的需要被重新调度的标志位。
- 当一个进程被唤醒时,如果发现被唤醒的进程比当前进程更应该被执行,也就是具有更高的优先级,那么当前进程的需要被重新调度的标志位也将被设置。
2.6内核是抢占式内核,不过,不知道从哪个版本开始,2.6内核引入了两种抢占方式:
- 自愿地抢占:这个名字实际上有些奇怪,自愿了还能叫抢占么,好象有纯属犯贱的意思,哈哈!实际上,这个叫法还是有它的道理的,请听我慢慢分析。这个抢占,实际上是通过在内核中添加了很多调度点实现的,那么这些调度点需要如何添加才更安全有效呢?聪明的内核开发人员,别出心裁想到了一个好办法。呵呵,是不是有些急了,行了,我也就不卖关子了。内核中的一些函数是有可能引起睡眠的,所谓睡眠在内核中无非是将自己添加到一个睡眠队列里面,然后通过schedule函数将CPU主动让出去(请注意,睡眠的进程,必须要有人唤醒才行,他们之间的关系和睡美人和王子的关系有些类似)。大家应该明白了,这样的函数的运行环境肯定是可以调度的进程上下文,不错,可调度点就放在这些函数的入口处。有人稍作冥思苦想之后,不禁又要问了,既然这些函数都有可能调用schedule函数进入睡眠,再加调度点又有什么用呢?问题就发生在这些函数是可能睡眠而不是必须睡眠,如果不发生睡眠,加上的调度点无疑还是增加了进程程被调度的机会。问题讨论到这里,大家应该不用解释为什么叫他自愿地抢占了吧!
- 抢占:不同于自愿式抢占,并且这两种抢占通常也是鱼和熊掌的关系。他将调度点安放在内核从中断或者是异常返回内核空间的部分,如果此时内核处于非原子上下文(如果内核从嵌套的中断返回到嵌套以前的中断,这个时候也属于原子上下文),对于进程来说就是未持有各种互斥锁和信号量,并且当前进程的需要被重新调度的标志被置,那么将发生调度,这个调度也就又称为抢占了,有点儿谋朝篡位的意思!
由此引发的注意事项:2.4内核中处于内核空间的进程是不能被调度的,除非是它自己调用了schedule()函数的假设在2.6内核中不再成立。要想避免被调度,只能通过手动的调用preempt_disable()关闭抢占。当然,如果进程位于原子上下文也是不能被抢占的。
当前的CPU正在向多内核方向发展,所以真正的进程并行执行的情况将逐渐普遍,所以编程的时候,
不要假设只有一个CPU的情形,注意保持程序的普遍性。举例来说:上面提到如果进程和中断共享某个数据结构,那么进程访问这个数据结构的时候需要关闭中断,这样做就足够了么?如果是单CPU,显然这样做不会有任何问题;但是,如果是多CPU的情形,那么就会产生问题,因为禁止的只是当前CPU下的中断,其它CPU也有可能产生并响应中断,这样就无法保证数据的一致性了。所以,更加安全的做法是进程不仅要关闭当前CPU的中断,而且用自旋锁对数据进行保护,如调用spin_lock_irq,中断函数也需要用spin_lock取得数据的访问权。
本文主要讨论何时需要对数据进行互斥保护,至于具体的互斥方法,还是需要大家在内核编程的实践中,逐渐积累。
参考文献:2.4和2.6内核源码
LDD 3
Linux内核源代码情景分析
阅读(1742) | 评论(0) | 转发(0) |