Linux 内核开发特点

开发的特点

相对于用户空间内应用程序的开发,内核在开发上是有一些区别的。尽管这些区别不会增大内核开发的难度,但是依然与开发用户代码有很大不同。
最主要的差异有以下几种:

1.内核编程时既不能访问 C 库也不能访问标准的 C 头文件;
2.内核编程时必须使用 GUN C;
3.内核编程时缺乏内存保护机制;
4.内核编程时难以执行浮点运算;
5.内核只分配给每个进程很小的定长堆栈;
6.由于内核支持异步中断、抢占和 SMP,因此必须时刻注意同步和并发,
7.要考虑可移植的重要性。

无 libc 库抑或无标准头文件库

内核不能链接使用标准的 C 函数库,而且其它的库也不行。造成这种情况的主要原因是速度和大小,对于内核而言,完整的 C 库或者仅仅是其一个子集,都太低效且大了。

针对这样的情况,大部分常用的 C 库在内核中都已经得到了实现。
当然了,也存在有没有实现的函数,例如 printf() 函数。内核代码虽然无法调用 printf() 函数,但是它提供了 printk() 函数,与 printf() 函数几乎相同。printk() 函数负责把格式化好的字符串拷贝到内核日志缓冲区上,syslog 程序就可以通过读取该缓冲区来获得内核信息。

GUN C

Linux 的内核是由 C 语言编写的,但是内核并不完全符合 ANSI C 标准。实际上,内核开发者总是用到 gcc 提供的许多语言的扩展部分。(gcc 是多种 GUN 编译器的集合,它包含的 C 编译器既可以编译内核,也可以编译 Linux 系统上用 C 语言编写的其它代码。)

没有内存保护机制

如果一个用户程序试图进行一次非法访问内存,内核就会发现这个错误,发送 SIGSEGV 信号,并结束这个进程。如果是内核自己非法访问内存,那么结果很难控制了。内核中发生内存错误会导致 oops。(当某些比较致命的问题出现时,我们的Linux内核会抱歉的对我们说:“哎呦(Oops),对不起,我把事情搞砸了”。Linux内核在发生kernel panic时会打印出 Oops 信息,把目前的寄存器状态、堆栈内容、以及完整的 Call trace 都 show 给我们看,这样就可以帮助我们定位错误。)

在内核中,不应该去做访问非法的内存地址,以及引用空指针之类的操作。此外,内核中的内存不分页。也就是说,每用掉一个字节,物理内存就减少一个字节。

不要再内核中轻易使用浮点数

在用户空间的进程内进行浮点操作的时候,内核会完成从整数操作到浮点数操作的模式转换。在执行浮点指令时到底会做些什么,因体系结构不同,内核的选择也不同,但是,内核通常捕获陷阱并着手于整数到浮点方式的转变。

与用户空间进程不同,内核并不能完美地支持浮点操作,因为它本身不能陷入。 在内核中使用浮点数时,除了要人工保存和恢复浮点寄存器,还有其他一些琐碎的事情要做。如果要直截了当地回答,那就是:别这么做了,除了一些极少的情况,不要在内核中使用浮点操作。

浮点数的编码跟整数编码是不一样的,计算时需要专门的寄存器和浮点计算单元来处理,一个浮点运算指令使用的CPU周期也更长。因此对于内核来说就会想尽量回避浮点数运算,譬如说浮点数经过定点整数转换后进行运算,效率会高很多,即使CPU带有浮点数运算部件,一般内核还是要避免直接进行浮点数运算,因为这些部件有可能被用户进程占用了,内核要判断这些浮点数部件是否被占用,保护现场,然后用浮点运算部件计算结果,恢复现场,开销会很大。如果CPU不支持浮点数运算,也就只能软件实现浮点数运算,要是你设计内核的话,相比起多做额外功夫(写浮点数实现代码)并且使得内核的效率不高,大家更加情愿避免这个问题。

容积小而且固定的栈

用户空间的栈比较大,且可以动态增长,所以用户进程可以从栈上分配大量的空间存放变量,甚至包含巨大的结构体和数组。而内核的栈随体系结构而变化,到那时固定而小,每个处理器都有自己的栈。

同步和并发

内核很容易产生竞争条件。这是因为内核的许多特性要求能够并发地址访问共享数据,这就要求有同步机制以保证不出现竞争条件,特别是:

1.Linux 是抢占多任务操作系统。
2.Linux 内核支持对称多处理器系统(SMP),两个或多个处理器上执行的中断代码可能会同时访问共享的资源。
3.中断是异步到来的,完全不顾及当前正在执行的代码。如果不加以适当的保护,中断完全可能会在代码访问资源时到来,这样中断处理函数就可能访问同一资源。
4.Linux 内核可以抢占,内核中一段正在执行的代码可能被另一段代码抢占,从而导致几段代码访问相同资源。

常见的解决办法:

1.自旋锁(spin_lock),是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

2.信号量 (Saphore) 是用来解决进程/线程之间的同步和互斥问题的一种通信机制,是用来保证两个或多个关键代码不被并发调用。信号量由一个值和一个指针组成,指针指向等待该信号量的进程。信号量的值表示相应资源的使用情况。信号量 S>=0 时,S 表示可用资源的数量。执行一次 P 操作意味着请求分配一个资源,因此 S 的值减 1;当 S<0 时,表示已经没有可用资源,S 的绝对值表示当前等待该资源的进程数。请求者必须等待其他进程释放该类资源,才能继续运行。而执行一个 V 操作意味着释放一个资源,因此 S 的值加 1;若 S<0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。 信号量是选择睡眠的方式来对共享工作停止访问的。也就是说信号量通过PV操作同步解决了进程/线程对临界资源利用的冲突问题。

可移植性的重要性

尽管用户空间的应用程序不太注意移植问题,然而Linux却是一个可移植的操作系统,并且一直保持这种特点。也就是说,大部分C代码应该与体系结构无关,在许多不同体系结构的计算机上都能够编译和执行,因此,必须把体系结构相关的代码从内核代码树的特定目录中适当地分离出来。

诸如保持字节序、64位对齐、不假定字长和页面长度等一系列准则都有助于移植性。

参考文献

《Linux 内核设计与实现(第三版)》

猜你喜欢

转载自blog.csdn.net/bingfeilongxin/article/details/87258383