内核编程与应用编程对比

来源:互联网 发布:java搭建游戏服务器 编辑:程序博客网 时间:2024/06/03 00:03

内核编程与应用编程对比

我虽然一直比较喜欢研究底层技术,也经常阅读Linux内核源码,但是工作以来,却没有真正从事过内核编程的开发。即使后来做的负载均衡,也是在应用层处理网络数据包——虽然跟普通的应用编程区别也很大吧。

直到目前的工作,才是真正从事内核方面的开发——没办法啊,小公司暂时还没有精力做应用层的协议栈。即使有netmap和dpdk这样现成的框架,以及轻量级的开源的应用态协议栈lwip。将数据包映射到用户空间,一方面增加了很多额外的工作量,如内存管理,连接管理控制等,同时还不能使用netfitler(iptable)已经提供的功能——尽管效率是低了点。

虽然我没有真正的内核开发经验,但是凭借着以往的经验,我还是勇敢的负责了内核的开发工作——不勇敢也不行哈,没有别人。另外由于在内核做的也是相对独立的网络模块,所以到目前为止,自我评价还不错。

到现在为止,已经做了三个多月了,基本上没出过什么大问题。期间也遇到不少小坑,也解决了不少问题,因此特意写篇博客记录和分享一下—— 目前刚刚切入正题,我还是挺擅长跑题的O(∩_∩)O哈!。也可以给立志从事内核开发的朋友一点经验和心得吧。

目前,内核编程给我最大的感触是程序的执行流比较多,并发逻辑比应用编程要复杂的多。这个“执行流”是我杜撰的名词,但基本上可以表达我的意思。应用编程中,谈到并发,无非是多进程多线程,一般对共享资源使用锁保护就基本上没有问题了。一个线程可以视为一个执行流,除非被信号打断,该线程都是按照代码顺序执行。也就是说,我们在应用层编写的代码和业务逻辑,只会被我们定义的线程或者进程执行。信号处理函数一般情况下,都会写的比较简单,大多是设置标志位。而在内核中,有中断,软中断,定时器,还有系统调用等诸多会涉及业务逻辑的执行流。由于内核自身的特性,对共享资源的保护,也要斟酌使用不同的手段。

对于某些共享资源,有时候使用spin_lock进行保护,但随着功能需求的增加。需要加入与用户空间的交互,在代码实现上,有时候会直接调用现成的代码。结果那些代码中对共享资源的保护使用的是spin_lock,而数据包转发的业务逻辑代码都是运行在软中断中,结果造成了死锁。

我还修正过一些别人写的bug。其中有一个bug,给我留下的印象也很深。当时产品总是不定期重启,而我们这里又无法重现。我当时刚刚接触已有的产品代码,对于这种重启的bug,在不能重现的情况下,我选择review代码这看似笨重却非常有效的方法。还好产品的关键功能的代码量不算多,花了2天的时间把大部分代码读懂,同时顺手修正了一些有可能造成重启的问题。客户升级以后,大部分没有问题了,但还是有个别重启的现象,那么这意味着还有漏网之鱼。当时我基本已经把关键流程全部理通了,修正这个问题的流程很有意思。我靠在椅背上,眼睛望向天花板,心里把数据包从入口到出口的流程走了一遍,并考虑所有的分支和特殊情况。然后Get it!大概花了不到15分钟的时间。然后看代码,验证自己的猜想。

造成重启问题的原因如下:因为某种业务逻辑需求,申请了一个动态结构,并设置了定时器超时,到期释放。当业务逻辑访问这个动态结构时,会刷新它的访问时间,延长其生命周期。但是在某些情况下,可能需要提前删除这个结构时,会调用del_timer删除定时器,然后释放内存。看到这样的代码,我立刻就怀疑当del_timer删除定时器时,如果该定时器正在处于执行阶段,怎么办?上网查询了一下,果不其然,del_timer返回时不能保证没有正在执行的定时器。那么当定时器还在执行的时候,这个动态结构就被释放了,定时器也会随着动态结构的释放而释放。这样的代码肯定是有问题的。如何解决这个问题呢?第一个念头,就是保证同步删除定时器。根据搜索的结果,可以使用del_timer_sync。然而我仔细一想,这样仍然有问题。本来这个动态结构是使用定时器来释放,但是这里确实强制释放,那么即使使用了del_timer_sync停掉了定时器,那么这时定时器可能已经完成了超时,并释放了动态结构。这时再强制释放等于double free。同时del_timer_sync还有一个问题,这种同步操作,必然带来性能上的下降。所以最终的选择方案是增加一个标志,在强制删除时,将标志置位,保证释放操作只有一个执行者,同时引入引用计数。

最近,为了优化性能,我也引入了两个bug,还好都及时修正了。bug造成的原因,还是由于对linux内核本身不太熟悉造成的。其中一个最近发现的bug,居然花费我一天的时间才找到原因。当使用某个应用程序时,会造成内核崩溃。起初我一度甚至怀疑这是内核的bug——虽然我觉得不大可能,于是我就开始验证排除这个可能。因为不开这个应用程序时,内核模块完全没有问题。打开应用程序时,内核就会崩溃。而这个应用程序跟内核模块,完全没有任何的交互。后来分析这个应用程序的代码,与网络关系紧密的就是注册了一个PF_PACKET的socket,用于抓取所有网卡的数据包。于是我去查看了相关代码,当PF_PACKET的接受包函数,会检查skb是否被共享,如果是共享的就clone一份,ip_rcv入口处也有类似的代码。那么当该应用程序运行时,就意味着ip_rcv会检测发现这个skb是共享的,于是就会clone一份。这就是该应用程序运行时与不运行时,内核处理数据包流程的最大区别。于是,我修改ip_rcv的代码,不再坚持skb是否是共享,而是直接clone。果然,内核在不启动该应用程序时候,依然崩溃。这样就证明了,问题还是出自自己的代码处,而且是与skb相关的代码。经过一番查找,最终找到了根本原因。

我在netfilter的两个hook点上,注册了两个hook函数。前一个钩子函数,初始化了一些per cpu变量,后一个钩子函数,简单检测了per_cpu->skb与hook的参数skb如果是相等的情况,就不再初始化,直接使用per cpu的变量了。造成问题的原因就在于,在有skb_clone调用时,不同hook调用时,skb->data发生了变化。第二个hook位置,skb->data指向的内存与第一个hook处不一致。但是skb_clone本身并不会造成这样的结果。这说明在netfilter的不同hook之间,当skb被clone了,会重新分配skb的数据空间——具体是哪处代码,我暂时没有找到。

这个bug让我吸取了一个教训。内核编程,由于你不可能熟悉linux内核所有的代码,所有在编程中,要想着,除非内核已经明确定义的行为,才能放心使用。不是明确定义的行为,不能根据平时简单的测试,就确信没有问题。如上面的例子,内核从来没有说过两个hook点之间,skb是一样的,skb的数据空间即skb->data是一样的。

对于在linux内核实现网关的某些功能时,我发现,虽然linux已经提供了很多现成的东西,可以保证快速开发。但是内核本身架构是一个通用计算机,不是专门针对网络处理的。其网络模块的架构本身有很多弊端和不便处,尤其是对比我前公司的产品架构——该架构看上去挺简单的,但越体会越能感觉,简单就是美!就是效率——一个是产品效率即性能,还一个是开发效率。

Note: 其实做网络设备的,做到高性能的产品,大部分架构都比较相似,但在细微处的不同,造就了不同的产品性能。

原创粉丝点击