Linux内核设计与实现(1)--内核开发的特点

1. 内核编程时既不能访问C库也不能访问标准的C头文件

       其中的原因有很多种。其一,C标准库的很多函数实现都是基于内核实现的,这内核编译的时候都还没有内核,所以就不存在这些函数,这个就是先有鸡还是先有蛋这个悖论。其二,其主主要的的原因是熟读和大小。对于内核来说,完整的C库–哪怕是它的一个子集,都太大且太低效了。
       不过大部分的C库函数在内核中都已经得到实现。其实现源码在lib/***.c,对应的头文件在include/linux/文件夹中,只需要包含<linux/xxx.h>头文件,就可以使用它们了。

2. 内核使用GNU C

       Linux内核都是用C语言编写的。但是内核不完全符合ANSI C标准。它是ANSI C的扩展,包含一些GNU C独有的特性。具体的区别可以参考 https://blog.csdn.net/lbit20131014/article/details/81108096
       开发者采用gcc编译内核同时也可以用gcc编译系统应用程序(gcc是多种GNU编译器的集合,即可以编译内核,也可以编译Linux系统上用C写的其他代码)

3. 没有内存保护机制

       如果用户程序试图进行一次非法的内存访问,内核就会发现这个错误,发送SIGSEGV信号,并结束整个进程;这个内存保护机制是内核来实现,而在内核编写的时候如果出现非法内存访问那就很难控制了,所以设计的时候一定哟注意内存错误的问题

4. 不要轻易在内核中使用浮点数

       

5. 容积小而固定的栈

       用户态程序栈的大小可以是比较大的,同时还可以动态地增长。而内核的对战大小都是固定的,其准确大小随着体系结构而变化,以下是网络引用。

Linux内核栈溢出(stack overflow)问题
最近一段时间在设计和开发一个Linux内核模块,进入到最后的正确性测试与稳定性测试阶段。在这个阶段发现了一个非常有意思的问题,堆栈溢出(stack overflow)。Linux内核堆栈溢出之后直接导致了系统kernel Panic。由于导致stack overflow的原因是递归调用导致的,所以,最后通过调试串口导出的kernel panic信息很快就定位问题所在了,否则这样的问题还真是很难调试和发现。通过这次bug,我们应该记住的是:Linux内核stack资源是有限的,而递归调用将大量消耗stack资源,因此在内核编程中尽量少用递归算法,否则将会导致出乎意料的一些问题。依次类推,为了减少stack资源的消耗,程序的局部变量定义的不要太大,否则也将会消耗大量stack资源,从而导致内核程序的不稳定。
 
为了解决递归调用导致的问题,我将递归算法改写成了非递归算法,解决了stack overflow的问题。在此介绍一下递归算法改写成非递归算法的一些思想。在项目实现过程中,需要对IO请求进行按顺序排队,因此采用了效率较高并且实现简单的快速排序算法,该算法是一种分治算法,即将排序队列进行切分,分解成一系列的小问题进行求解,针对这种问题,很容易采用递归的办法进行实现,伪代码描述如下:
/* qs_sort实现从小到大的排序 */
Struct bio qs_sort(struct bio_list *list_head, struct bio *bio_tail) {
       Struct bio_list *less_list, *large_list;
       Struct bio *middle_bio;
 
       /* 递归调用结束点,小问题求解完毕,直接返回最后一个元素 */
If (!list_head) {
              Return bio_tail;
       }
      
/* 对队列进行切分,选择一个middle_bio,并且按照middle_bio将其切分成less_list队列和large_list队列 */
       Split_list(list_head, less_list, large_list, &middle_bio);
      
       /* 采用递归的方法实现大队列的排序操作 */
       Middle_bio->bi_next = qs_sort(large_list, bio_tail);
      
/* 采用递归的方法实现小队列的排序操作 */
       Return qs_sort(less_list, middle_bio);
}
 
有上述可见,采用函数递归的方法实现简单,但是将会牺牲(栈)存储空间,为此,需要将其改写成非递归的实现方法,非递归的实现算法可以点击此处下载(,欢迎提出意见)。改写的思想是将递归所采用的存储栈空间动态分配。递归算法本质上利用堆栈存储了切分的小问题,因此,可以采用系统内存动态分配存储空间,自己维护小问题堆栈。那么可以做到不利用函数堆栈空间,避免了栈空间的大量消耗。
总之,在内核程序实现过程中,一定要注意栈空间的使用,特别像递归这样的方法尽量少用,否则将可能会对产品产生致命的打击。

from:http://hi.baidu.com/mffppwbneqdmnqe/item/8761040489cddfd6dde5b098

6. 同步和并发

        内核很容易产生竞争条件,内核的许多特性要求能够并发地访问共享数据,这就要求内核有同步机制保证不出现竞争的条件。以下是应用网络博客。

造成并发执行的原因

中断,软中断和tasklet: 中断和进程, 中断和中断之间有可能会引起并发问题.

内核抢占: 一个线程会被另一个线程抢占, 所以线程和线程之间也有同步问题.

睡眠: 线程主动性睡眠也会引起同步问题.

对称多处理(SMP): 多个处理器同时执行一套代码就有问题.

 

从上面我们可以看出来, 如果不需要支持SMP的话, 我们只需要关闭中断或者抢占就可以解决并发问题(关闭中断之后, 被动的抢占不会发生, 只有线程主动调用某些API才会触发进程调度). 当SMP出现之后(linux2.0时代就已经出现了), 我们就必须使用自旋锁来解决同步问题了.

 

造成死锁的原因

递归引起的死锁: linux下的spin_lock是不支持递归的, 所以同一个线程不能多次获取同一个锁.

ABBA引起的死锁: 多个线程获取锁的顺序不一致引起的死锁. 解决方法是大家都按相同的顺序去获取锁.

 

Linux内核中的同步方法

自旋锁: 可以在中断上下文中使用. (不会引起死锁, 如果线程和中断处理函数共用了一个锁的话, 那么当线程获取自旋锁之后是会明确的关中断的. Spin_lock_irqsave)

信号量: 会引起睡眠, 所以不能在中断上下文中使用.

互斥体: 很容易理解, 这里需要注意的是互斥体只能被同一个线程获取和释放.

读-写自旋锁: 适用于读者和读操作很多, 但是写者和写操作很少的情况. 需要注意的是, 如果读者过多的话, 会造成写者饥饿.

读-写信号量: 基本同读写自旋锁, 然后像信号量一样会引起睡眠.

完成变量: 一种简单的异步通知机制, 通过名字就很容易理解.

顺序锁: 顺序锁与读写自旋锁很类似, 只是该锁的写会优先于读, 也就是写者不会让读者饥饿.

Linux提供了这么多种不同的方式来做同步, 就是为了在不同的应用场景中找到性能和效率的平衡点. 我们要权衡CPU占用和进程调度带来的开销, 在不同的场合下选择不同的加锁机制.
--------------------- 
作者:shinezhang86 
来源:CSDN 
原文:https://blog.csdn.net/shinezhang86/article/details/48292565

7. 可移植的重要性

        因为Linux是一个可移植的系统,大部分代码与体系结构无关,同时,作为一个专业的编程者来说,应该要注重系统的可移植性

猜你喜欢

转载自blog.csdn.net/weixin_43369409/article/details/83152775
今日推荐