以menu项目为例的软件工程分析

在听完孟老师这节课的讲解后,原本稀稀拉拉的软件工程知识被重新构建起来。也接着这次作业和menu项目,重新回顾下知识点,那我们开始吧!

1. 编译和调试环境配置

我是用的是windows上的vscode,通过remote-ssh连接到虚拟机进行编译调试,使用make进行构建,使用gdb进行调试

1.1 vscode及其插件安装

到官网 https://code.visualstudio.com/ 下载安装包 进行安装即可,装完之后装好remote-ssh c/c++插件,c/c++可能安装失败,需要手动下载安装包安装或者挂上梯子进行下载。

并连接上虚拟机,在命令行中用 git 克隆 menu项目的源码,地址为 git@github.com:mengning/menu.git。因为在linux虚拟机下开发,一般自带 make gcc等工具,所以我们不需要再进行安装

1.2 环境配置

插件安装成功之后,我们需要针对项目配置开发和调试的环境。在vscode中有三个文件和这部分相关,具体如下:

  • tasks.json  用于说明如何构建程序
  • launch.json 同于配置debugger
  • c_cpp_properties.json 用于指定编译器路径和IntelliSense设置

c/c++插件可以给我们默认配置好这些文件,大部分情况下我们无需进行修改,流程如下:

1. 点击Terminal ->  configure default build task -> create tasks.json from template -> c/c++ gcc build active file 即可,如下图所示

 

2. 点击 debug -> add configuration -> c++(gdb/lldb) 即可

由于使用make构建,这里我们先修改下Makefile文件,改动后如下

CC_PTHREAD_FLAGS             = -lpthread
CC_FLAGS                     = -g -c 
CC_OUTPUT_FLAGS              = -o
CC                           = gcc
RM                           = rm
RM_FLAGS                     = -f

TARGET  =   test test_fork test_reply test_exec
OBJS    =   linktable.o  menu.o

all: $(TARGET)

$(TARGET): % : $(OBJS) %.o
    $(CC) $(CC_OUTPUT_FLAGS) $@ $^

改动之后我们可以使用 make all 构建 test test_fork test_reply test_exec四个程序,也可以使用 make <name> 构建其中一个,如make test构建test, make test_fork构建test_fork, 更方便

再修改task.json的args部分进行修改

{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "shell",
            "label": "gcc build active file",
            "command": "make",
            "args": [
                "${fileBasenameNoExtension}",
                "-f",
                "${fileDirname}/Makefile"
            ],
            "options": {
                "cwd": "${fileDirname}"
            },
            "problemMatcher": [
                "$gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

最后测试一下,在test_exec.c的main函数其中一行下个断点,按F5即可开始调试,如图

通过对上面环境的配置,大致上学会了怎么配置vscode和怎么写Makefile,这些在以后的工作中还是很有用的

 

2. 项目总体分析

总的来看,menu项目包括以下几个源文件

  • hello.c        仅仅输出一行hello world,用于测试和test_fork中子进行的exec调用
  • linktable.c       封装了一个链表的定义及提供的相关操作接口
  • menu.c           用链表实现了一个menu,并提供相关操作接口
  • test.c         用menu实现了一个伪OS,输入命令可以输出结果,如打印时间
  • test_fork.c      在test.c 的基础上添加了fork的例子
  • test_exec.c    在test_fork.c 的基础上添加了exec的例子
  • test_reply.c    在test.c的基础上实现了一个网络通信的例子

从上面可以看出总体的代码结构非常清晰,注释也非常规范,让人一眼就能看出来各部分的功能。

这个项目在编译的时候需要注意一下问题:

  1. 使用 函数wait 的文件应该添加 #include <sys/wait.h>,否则编译器会产生warning
  2. 在有 fork调用的例子中,子程序执行完后应该执行exit(0)调用,否则主线程会一直卡在wait(NULL), 无法观察到child complete的输出,子线程会返回替代主线程,导致原来的主线程永远阻塞,所以需要添加一行exit(0);
    else if (pid == 0) 
    {
        /*     child process     */
        printf("This is Child Process!\n");
        exit(0);
    }

 

3. 代码风格分析

整个项目规范整洁,遵循了常规语言规范,合理地使用了注释,空格,缩进,空行等。让人一看就知道此文件的作用,编写时间,作者,内容等等

 代码逻辑清晰,在必要的地方加上了注释对代码进行说明,能让读者理解的更好

 

其他地方像命名,缩进等也做的非常规范,这里只放几个例子,就不一一列举了

  • 命名与程序里的含义保持一致
  • 花括号独占一行且对齐,适当添加空格保持可阅读性 

总的来说,这个项目在代码风格上非常统一,大家都需要向这个项目的代码风格看齐,争取也写出具有良好代码风格的项目,在写的时候可以借助一些如cpplint的风格审查工具来检查,帮助自己完成高质量的项目。

 

4. 模块化设计分析

模块化是在软件系统设计时保持系统内各部分相对独立,以便于每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离。关注点的分离也叫做模块化,是一种分而治之的方法,其思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。

因此,软件系统设计中的模块化程度成了评价一个软件系统设计的重要指标,一般我们使用耦合度和内聚度来衡量软件模块化的程度。耦合度是指模块之间的依赖程度,可分以下几种,一般我们追求松散耦合。内聚度是指一个软件模块中各个元素之间互相依赖的紧密程度,理想的内聚是一个模块只做一件事。 

 具体到Menu项目,作者从几个test例子中分离出来了都有用到的模块menu,并进一步将menu模块所需的数据结构LinkTable分离出来,形成一个新的模块,大致上形成了下图的结构,即test的每个例都可以使用Menu模块,Menu模块又会使用LinkTable模块的接口,

 使用者只要知道要使用的模块的接口就可以了,而无需知道其实现的细节。

如上,将这两个模块分离出来后,各模块只要封装好自己的数据,实现自己的功能,就能实现高内聚,低耦合,增强代码重用性,增强工作效率,也便于多人协同开发和测试。

 

5. 可重用接口分析

重用分为消费者重用和生产者重用。消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。

从上面可以知道,menu项目分离出了menu和LinkTable两个模块,查看远吗可以发现,两个模块封装了各自的接口,并向外界提供了必要的一些接口,这些接口可以在模块对应的头文件中找到

menu模块的接口如下

 LinkTable的接口如下

其中 SearchLinkTableNode 中 还增加了callback函数 的参数,这使得接口更加通用,能够适应更多样的需求。

而在使用方面,用户只要包含想要使用的模块的头文件,就能使用这些接口了,非常方便。

 

6. 线程安全分析

要分析线程安全,首先我们要知道可重入函数的概念。可重入函数就是可以由多于一个任务并发使用,而不必担心产生数据错误的一类函数。相反,不可重入函数不能由超过一个任务所共享,除非能确保函数的互斥。可重入函数可以在任意时刻被打断,稍后再继续运行,不会丢失数据。可重入要么使用局部变量,要么在使用全局变量时保护自己的数据。

可重入函数式线程安全的,而不可重入函数是线程不安全的。对于可重入函数一般有以下要求

  • 不为连续的调用持有静态数据
  • 不返回指向静态数据的指针
  • 所有数据由函数的调用者提供
  • 使用局部变量,或者制作全局变量的局部变量拷贝来保护全局数据
  • 使用静态数据或全局变量时做周密的并行时序分析,通过临界区互斥来避免临界区冲突
  • 绝对不调用任何不可重入函数。

以实际的menu项目进行分析,可以发现LinkTable的数据结构定义中包含了一个粗粒度的锁,用于实现该数据结构的互斥访问,同时在各个需访问该数据结构的函数中,穿插着加锁和解锁的操作实现互斥访问,通过这些操作和锁的使用,使得这些函数不会同时进入临界区。因为只使用了这一个全局变量,所以这些函数是线程安全的。

 

而对于menu模块而言,则不全是线程安全的函数。可以看到Menu模块有两个全局变量,见下图。

menu模块中对于head的访问都是用LinkTable模块的函数进行,上面分析过这些函数是线程安全的。而对于另外一个全局变量prompt的访问包括一处写入和一处读取,这两个地方是没有做互斥访问的,见下图

 所以这两处如果在两个线程中同时执行的话,结果是不确定的。但对于menu项目的例子而言,这种情况是不存在的,写入Prompt的SetPrompt函数调用的地方都在main函数的最开始处,这时并没有多线程,所以使用没有问题。

但是这两个函数本身并不是线程安全的,也不是可重入函数,因为可能存在两个线程同时执行两处代码。

 

 7. 总结

这次课,孟老师生动形象地带我们过了一遍有关模块化,可重用接口和线程安全的知识,并结合menu的例子给我们讲解地非常清楚。而这次作业,则是对整堂课的一个消化吸收,也是一次回顾,让我们在亲自分析中增强对这些知识的理解程度,更让我学会了Makefile的基本编写,可以说是收获良多,非常感谢!!!

 

参考资料: https://gitee.com/mengning997/se/blob/master/README.md#代码中的软件工程

 

posted @ 2020-11-08 19:06  三丁目的夕阳  阅读(111)  评论(0编辑  收藏  举报