代码中的软件工程

------------恢复内容开始------------

  本博客基于孟宁老师的menu项目案例,以VS Code + GCC运行环境。对代码开发中的软件工程思想做出一些思考。

相关资料:

  项目源码:https://github.com/mengning/menu

  资料:https://gitee.com/mengning997/se/blob/master/README.md#%E4%BB%A3%E7%A0%81%E4%B8%AD%E7%9A%84%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B

 一、环境配置

Ubuntu系统自带的gcc足够我们使用,clone项目的源码,在vscode中打开文件夹,安装c/c++的插件,在hello.c文件运行,选择调试的工具,即gcc,自动生成配置文件

我们需要修改launch.json

即将"externalConsole": false改为"externalConsole": true

在menu文件夹下,运行make all命令对项目进行编译,如果不编译直接打开test文件debug,会报错如下图所示

表示以上几个函数未定义就开始使用,虽然文件中用include,但是运行中找不到函数的具体实现,因此需要先所有的文件进行编译,Makefile文件中描述了整个软件工程的 编译规则和各个文件之间的依赖关系;我们使用make all命令进行编译所有文件

可以看到多出来几个文件直接运行./test查看运行后效果。

这里有四条命令,version,time,time-asm,quit。

其中quit命令需要完善,否则接到quit命令只会打印出Quit from MenuOS却没有任何操作,完善如下

int Quit(int argc, char *argv[])
{
exit(0);
}

test_fork.c、test_exec.c与test_reply.c这三个文件的运行需要修改makefile文件

修改TARGET以及OBJS的内容

 

二、代码分析

1.关于代码注释

  最精简的是无注释,理想的状态是即便没有注释,也能通过函数、变量等的命名直接理解代码。糟糕的状态是代码本身很难理解,而作者又惜字如金。最后是将函数功能、各参数的含义和输入/输出用途等一一列举,这往往是模块的对外接口,以方便自动生成开发者文档,下面给出了一个接口函数的注释范例。

2.模块化设计

  一般我们使用耦合度和内聚度来衡量软件模块化的程度。耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合、松散耦合和无耦合。一般在软件设计中我们追求松散耦合。内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性。简单而言类似于单线联系,每个模块只做自己相关的额事情,同时仅使用自己下层的服务,并仅对上层负责。模块化程序设计的基本思想是自顶向下、逐步分解、分而治之,即将一个较大的程序按照功能分割成一些小模块,各模块相对独立、功能单一、结构清晰、接口简单。有效控制了程序设计的复杂性、提高了代码的重用性、易于维护和功能扩充、有利于团队开发。在软件设计中我们追求松散耦合,将数据结构及它的操作与菜单业务处理进行分离处理,因此我们需要设计合适的接口,以便于模块之间互相调用。

  接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。换句话说,接口具体定义了软件模块对系统的其他部分提供了怎样的服务,以及系统的其他部分如何访问所提供的服务。接口规格是软件系统的开发者正确使用一个软件模块需要知道的所有信息,那么这个软件模块的接口规格定义就必须清晰明确地说明正确使用本软件模块的信息。一般来说,接口规格包含五个基本要素:

  接口的目的;

  接口使用前所需要满足的条件,一般称为前置条件或假定条件;

  使用接口的双方遵守的协议规范;

  接口使用之后的效果,一般称为后置条件;

  接口所隐含的质量属性。

3.针对本项目

在test.c、test_fork.c、test_exec.c与test_reply.c中分别实现了各自的操作,其中区别如下:

1.test.c只提供了基本操作,没有提供子进程操作;

2.test_fork.c提供了fork操作,可以在父进程中起一个子进程;

3.test_exec.c在test_fork.c的基础上提供了exec操作,在子进程中执行hello;

4.test_reply.c中通过起不同的子进程模拟客户端与服务器进行TCP通信。

在menu.c文件中存放着以上四个文件的相同的功能,不同的功能则分别放置在以上四个文件中,在menu中提供了统一的SetPrompt,MenuConfig,ExecuteMenu()等接口,当需要改变menu的实现方式时,不需要修改test的代码,方便软件开发的统一维护,节省软件开发的成本。同时在需要增加新功能时只需要在menu之上构建新的代码实现即可,不用再重复的实现menu中的功能。命令行终端具有许多命令,命令有命令的名字、选项、参数等属性,于是menu将命令抽象为一个DataNode结构体,故menu的下层还有一层封装即linktable.c,在这一层实现了对结构体链表的创建、增删改查、获取头节点、获取下一个节点等操作。需要使用此结构体数组的相关操作时直接调用此文件中的方法即可

4.可重入函数与线程安全

  可重入(reentrant)函数:当一个执行流因为异常或者被内核切换而中断正在执行的函数而转为另外一个执行流时,当后者的执行流对同一个函数的操作并不影响前一个执行流恢复后执行函数产生的结果。

  不可重入(non-reentrant)函数:当程序运行到某一个函数的时候,可能因为硬件中断或者异常而使得在用户正在执行的代码暂时终端转而进入你内核,这个时候如有一个信号需要被处理,而处理的这个信号的时候又会重新调用刚才中断的函数,如果函数内部有一个全局变量需要被操作,那么,当信号处理完成之后重新返回用户态恢复中断函数的上下文再次继续执行的时候,对同一个全局变量的操作结果可能就会发生改变而并不如我们预期的那样,这样的函数被称为不可重入函数。例如在进行链表的插入时,插入函数访问一个全局链表,有可能因为重入而造成错乱。

  可重入函数满足的条件:

(1)不为连续的调用持有静态数据;

(2)不返回指向静态数据的指针;

(3)所有数据都由函数的调用者提供;

(4)使用局部变量,或者通过制作全局数据的局部变量拷贝来保护全局数据;

(5)使用静态数据或全局变量时做周密的并行时序分析,通过临界区互斥避免临界区冲突;

(6)绝不调用任何不可重入函数。

  线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。线程安全本质上其实是内存的安全!

  在本项目中,针对结构体链表的增加,删除等操作均加锁以使线程安全,如下图所示

基本结构为:

pthread_mutex_lock(&(pLinkTable->mutex));
对结构体链表所做操作
pthread_mutex_unlock(&(pLinkTable->mutex));
三.结语
  在本次课程中,孟宁老师结合项目详细的分析了模块化设计、可重用接口、线程安全等知识在实际项目中的具体体现,让我对这些知识的理解更加深刻。
posted @ 2020-11-09 18:36  xieyupei  阅读(196)  评论(0)    收藏  举报