摘要:许多人认为在Linux内核上进行软件工程是不可能的,甚至根本就不需要软件工程。虽然软件架构可以通过C语言完成,但这不能满足驱动程序的实现,驱动程序仍需要在软件方面进行适当的设计。
原文链接:https://mairacanal.github.io/does-the-linux-kernel-need-software-engineering/
声明:本文为CSDN翻译,转载请注明来源。
作者 | MAÍRA CANAL
译者 | 孙若楠 责编 | 屠敏
出品 | CSDN(ID:CSDNnews)
Linux内核需要软件工程吗?对于那些寻求简短答案的人来说:是的,确实如此。
现在,我们可以深入探讨一个更详尽的答案。
软件工程是一种更系统的软件开发方法,它涉及软件生命周期的定义、工程实施、测试、管理、优化和改进。从这个角度考虑软件时,我们还必须考虑软件需求、设计、构建、测试和维护。
软件工程提高了软件的可维护性、可扩展性和安全性。此外,还可以更轻松地将测试添加到软件堆栈中。这种方法能够使软件更加强大。
一些软件工程术语的词汇表:
可维护性:衡量修复或改进软件工件难易程度的指标。完成产品后,必须继续修复错误、优化功能并重构代码以避免将来出现问题。
可扩展性:衡量扩展或缩小软件工件难易程度的指标。
可测试性:衡量软件工件测试难易程度的指标。
许多人可能认为在Linux内核上进行软件工程是不可能的,甚至根本就不需要软件工程。在我看来,这些信念来自于两种说法:
将C语言应用于软件工程是不可能的:有时软件工程只与面向对象的编程语言有关。
如果遵循上述信念,我们最终可能会得到设计不佳的代码。随着设计不佳代码数量的增长,我们将会看到重复代码、死代码、异常函数和Bug。
最糟糕的是:当庞大的代码库中包含大量不良代码时,可维护性会变得困难,软件质量也会越来越低。
所以,让我们先了解一下为什么这两个信念是错误的。
使用C语言的软件工程
你可能会问:当没有类的时候,该如何使用花哨的设计模式避免代码重复,并实现漂亮的多态性?
如果使用C++中的设计模式,大家可能会更容易理解和实现。在C++中,你可以创建一个层次结构来完成所设计功能的不同板块,并且该功能是开箱即用的。但我们可以将这些概念转化为C语言。
虽然C语言是一种结构化语言,但它可以用来编写面向对象的程序。从这个意义上说,库和结构是你用C语言实现软件工程的主要“盟友”。此外,你还可以使用函数指针来在C语言中创建多态性。
例如,倘若想用C语言写一个简单的队列,可以使用以下方法:
typedef struct Queue Queue;
struct Queue {
int *buffer;
int head;
int size;
int tail;
int (*isFull)(Queue* const me);
int (*isEmpty)(Queue* const me);
int (*getSize)(Queue* const me);
void (*insert)(Queue* const me, int k);
int (*remove)(Queue* const me);
};
/* Constructor and destructors */
void Queue_Init(Queue const me, (*isFullFunction)(Queue* const me),
(*isEmptyFunction)(Queue* const me), (*getSizeFunction)(Queue* const me),
(*insertFunction)(Queue* const me, int k), (*removeFunction)(Queue* const me));
void Queue_Cleanup(Queue* const me);
/* Operations */
int Queue_isFull(Queue* const me);
int Queue_isEmpty(Queue* const me);
int Queue_getSize(Queue* const me);
void Queue_insert(Queue* const me, int k);
int Queue_remove(Queue* const me);
Queue *Queue_Create(void);
void Queue_Destroy(Queue* const me);
请注意,之所以用这种方法实现多态性,是因为我可以创建一个继承队列的新结构,比如:
typedef struct CachedQueue CachedQueue;
struct CachedQueue {
Queue *queue;
/* new attributes */
char name[80];
int numberElementsOnDisk;
/* aggregation in subclass */
Queue *outputQueue;
/* inherited virtual function */
int (*isFull)(CachedQueue* const me);
int (*isEmpty)(CachedQueue* const me);
int (*getSize)(CachedQueue* const me);
void (*insert)(CachedQueue* const me, int k);
int (*remove)(CachedQueue* const me);
/* new virtual functions */
void (*flush)(CachedQueue* const me);
int (*load)(CachedQueue* const me);
};
这就是C语言中的多态性。如果想要了解学习更多关于C 语言的多态性,在此也推荐大家可以阅读世界著名的作家及演说家Bruce Powel Douglass所著的《Design Patterns for Embedded Systems in C》一书,相信从中能够收获一定启发。
可以看到,花哨的软件架构其实是可以在C语言上完成的。Linux中有一些漂亮的抽象使用了这些概念,比如虚拟文件系统(VFS)。此外,一些库提供了很好的API,如DRM子系统。
但有时这不适用于驱动程序的实现。这就把我们带到了下一个问题:驱动程序需要在软件方面进行适当的设计。
在这里,我必须说:个人对VBA库有偏见。上个月,作为开发的GSoC项目(GSoC是Google举行的一个全球性项目,旨在为学生们和开源、自由软件、技术相关的组织建立联系,让学生们贡献代码并获得报酬)的一部分,我一直在为这个库编写单元测试。我对代码的重复量以及大量的函数印象深刻(也许不是很好)。
这并不是对AMDGPU(AMD用于Linux上GPU的完全开源统一内核驱动程序)代码的抨击:AMD为自由软件社区做了出色的工作,而且为一家大型图形零售商提供了开源驱动程序,这令人难以置信。此外,我确信这个问题也存在于内核的其他部分,所以我相信这是一个很好的讨论点。
让我们从“驱动程序是有限的”这个前提开始:你可以获取数据表、将硬件编码到功能结束并完成驱动程序。
但是,硬件公司通常不会开发具有独特特征的单一产品:他们通常会创造一个产品线,有时产品线会有“孩子”(子产品),即产品会不断迭代升级。
产品线有“孩子”......对于OOP程序员来说,这听起来像是一个美好的继承案例。
那么,如果你有一个产品线,你会为每个产品创建一个文件吗?对于增加了一些新功能的产品,你会粘贴以前的驱动程序并修改几百行代码吗?这似乎不是一个好的选择,原因如下:
可维护性:在项目维护阶段,代码越少越好;
你看,这一切都可以归结为可维护性。
作为代码重用的一个很好的例子,你可以查看IIO子系统。Maxim和Analog Devices Inc等硬件制造商通常拥有共享相同寄存器映射或共享功能的芯片。开发者无需为每个芯片创建驱动程序,而是编写一个驱动程序并将兼容的设备ID添加到设备表中。例如,你可以检查Maxim MAX1027 ADC驱动器,该驱动器与MAX1027、MAX1029、MAX1031、MAX1227、MAX1229和MAX1231兼容。因此,我们为六个设备提供了一个驱动程序:这对可维护性来说是非常好的!
在这种情况下,如果发现了一个Bug,我可以做一次修改并发送一个补丁,维护者只需审查一次,一切都将顺利进行。
然而,现实来看,当打开AMD Display Core中的DML文件夹,更确切地说是DCN20和DCN21中的display_mode_vba文件。这些产品线十分相似,所以我们也许可以重复使用很多代码。
但是,如果你检查这个目录,可以看到有三个不同的文件:display_mode_vba_20.c、display_mode_vba_20v2.c和display_mode_vba_21.c。
通过以下方式检查文件之间的差异:
$diff drivers/gpu/drm/amd/display/dc/dml/dcn20/display_mode_vba_20.c
drivers/gpu/drm/amd/display/dc/dml/dcn20/display_mode_vba_20v2.c
此时,如果发现了一个错误,我需要做三次修改。此外,我甚至可能不知道代码是重复的,所以我可能只在一个地方修复错误,并未对其他文件加以处理。然后,另一个开发者可能会再次发现同样的错误,并将其发送给维护人员,维护人员也不得不再次审查它。这会带来大量的返工!
如果我能猜出AMD多次复制和粘贴代码的原因,我会指出另一个可维护性的问题:函数非常庞大!一些VBA文件中的函数超过千行。
VBA文件中的庞大函数意味着,如果你想修改新产品线中的几行,需要复制和粘贴整个函数。
理想情况下,根据《Clean Code》一书的原则,我们希望拥有一些提供简单且提供单一功能的函数。我知道:这并不适用于100%的情况,但我找不到一个很好的理由让一个函数变得如此庞大并有几十个参数。
除了可读性之外,那些巨大的函数也对堆栈造成了相当大的伤害。
巨大的函数确实损害了代码的可读性、可理解性和可测试性。此外,由于该函数具有许多副作用,它们难以避免代码重复的问题。
一些软件工程术语的词汇表: 可读性:衡量软件工件阅读难易程度的指标。
可理解性:衡量软件产品理解难易程度的指标。
但对于AMDGPU的DML代码来说,这并不是一个死胡同。我的意思是,AMDGPU驱动程序在Linux上工作得十分出色,而且代码重构始终是一种选择。
解决此问题的一种方法是通过单元测试来确保代码被正确重构。但是在整个GSoC项目中,我注意到,为一个千个功能编写单元测试是不可能的。巨大的功能具有很多副作用,对每一个副作用都进行测试是不可行的。
也许对于显示模式来说,VBA单元测试不是唯一的方法。我们可以先把函数分解成更小的独立部分,因为这将有助于创建更好的测试、提高可读性,并减少堆栈大小。
现在,有了更小的函数,在DCN上共享代码并为其创建通用接口更加可行。
这种重构可以让我前面谈到的那些设计模式得以使用,使DML更易于维护和可读。我们可以考虑使用继承,通过一个基础库,DCN20可以从中扩展,然后DCN21可以从DCN20扩展。这就是那三个大文件变成小文件的方式。
创建一个通用接口:这就是设计模式的作用。
这种方式能够让我们在单元测试不可行的情况下进行更安全的重构。这并不意味着不会在这个过程中引入任何错误,但有一个结构化的计划会帮助我们避免这些错误。