在开始这篇富有某种奇妙感觉的文章之旅时我们先短暂的讨论一下关于软件开发方法论的简要:
纵观软件开发方法论,从瀑布模型、螺旋模型、RUP(统一软件开发过程)、XP(极限编程)、Agile(敏捷开发)一路走来,他们的好他们的美,我想接触过的人都会口口称赞,都是大师们一身的经验结晶最后沉淀为专业的技术方向、技术领域,带领我们软件开发者们永无止境的前进,目睹一场又一场的美景一桌又一桌盛宴。他们在不断的开辟新的领域,称为伟大的科学家一点都不为过。
但是为什么这么多方法论都没有能在企业中大面积的普及和使用或者说未能取得理想的效果呢,难道说是我们都不会吗?当然不是,我想我们程序员都是很聪明而且很富有创造性思维的人群,我们敢于改变现状追求真理,但是时间过去了很多,我们似乎都没有真正解决复杂软件的设计问题,我们参考很多书籍,数不胜数,扩展类、模式类、模型类太多太多,但是问题的核心始终未能触碰到,在黑暗中无数的摔倒都未能找到突破口。为什么DDD(领域驱动设计)能被我们接受并且愿意花时间花精力去学习去实践它,因为它发现了复杂软件设计问题的核心解决方法(Model Driven Develop 模型驱动开发),聚焦复杂系统的核心,并且有一套完整的框架、流程指导我们进行相关DDD的设计、开发工作 。
在DDD未出现在我们面前时,我们遇见复杂且庞大的业务系统的时候,会手忙脚乱的乱折腾,会发现根本无法拿下这么一个庞大的Monster,最后项目就算侥幸成功也只是依靠个人力量英雄主义般的在独自一人战斗,加班、熬夜精神极度集中,燃烧生命最后就算能取得成功,但往往系统最后还是这里出错那里出错,甚至还会漏掉什么功能没做,这算是一个常态,不足为奇了。
现在我们有办法改变这种局面,团队是干什么的?团队不是样子好看,三个臭皮匠赛过诸葛亮,只有一起参与系统的分析、设计才能最大化的保证需求的稳定性,最起码做到一起评审分析方案。别说我太理想化,难道这不是我们共同的期盼吗?
总而言之使用DDD的朋友都能感受到它的不一样,爱惜生命的朋友请开始和我一起DDD(领域驱动设计)之旅吧!
1.1】示例介绍【OnlineExaminationSystem】经过前面两篇文章的讲解,我们算是对DDD有了一个初步的认识,对它的概念它所提倡的开发原则开发思想有了一个基本的了解。任何方法论都要能被技术化落实到代码上才行,要能真正的为我们解决问题,所以我们这里使用DDD进行一个完整的系统分析、设计、开发来验证它确实如所说的那么好,接受一个新的东西往往需要一个时间过程,所以文章可能会有点长,基本上都是一些理论;
前面两篇文章的地址:
1.NET领域驱动设计—初尝(一:疑问、模式、原则、工具、过程、框架、实践)
2.NET领域驱动设计—初尝(二:疑问、模式、原则、工具、过程、框架、实践)
初次接触DDD的朋友可以先阅读一下上面两篇文章,算是有一个宏观的理解,DDD是如何颠覆传统的设计方法的。
项目背景介绍:
为了保证实践项目的全面性,这里挑选了我一直比较关注的教育行业信息化的一块【在线考试系统】作为实践的项目,使用了C/S、B/S混合型的系统结构,系统的业务范围主要是一个面向学校的学生在线考试系统。学生通过客户端(C/S)进行在线答卷,答完卷后再通过远程服务进行答题数据提交。老师通过后台(B/S)进行试卷的打分,最后得出所有学生的成绩数据并且在成绩公告栏中显示排名。(当然由于时间关系,示例代码可能推迟一点时间发布;)
扩展:
教育类系统都存在一个问题就是【老师】、【学生】、【家长】三者之间是没有任何信息化联系;对于像考试类的管理都没有任何方式告知家长学生的成绩情况,包括最近的成绩趋势,还有就是学生的整体对比度等等。21世纪什么最重要?人才;现在的家长都迫切的想知道每一天学生在学校的情况。所以对于这样的需要很有价值去分析,去实践起来,当然前提是教育需要进行改革才行。
1.1图
这是一个形象的需求思维导图,很形象的描述了我们系统的大概功能,最重要的是能表达真实的业务场景,这也是模型驱动开发的首要思想。用专业的领域驱动设计思想来描述的话上图那就是(解释性模型)带来的效果,如果我们用很抽象的程序专业图形来表达业务需求很难说明问题,所以我们在前期与业务人员沟通大概需求时基本上给出我们人类天生就能理解的图形化表示,可以视它为“解释性模型”;当然由于时间关系并不会完成上面所有的需求;
在以【School DataCenter】为中心的【Student】学生、【Teacher】教师、【Parents】家长、【Admin】管理员、【CEO】最高执行人,各角色分别处理一个业务环节上的不同操作;
这只是简单的项目介绍,目前大概就这些基本的需求,后面我们会进行详细的系统分析。业务都具有发散性特点,看似简单最后还是会出现很多需求问题,这也符合我们所需要的要求。从上图中我们可以很简单的就明白系统的基本功能,但是毕竟是一个简单的需求草图,只是我们脑子里的一个雏形,我们需要把它落实到真正的项目中去,这个过程非常的不容易,唯一能正确穿过迷雾的方法是对照模型进行分析、设计。
1.2】系统分析、建模上小节中我们基本了解到了系统主要有哪些功能而已,这一节我们将详细的对系统进行业务分析,当然主要是为了突出领域建模的重要性,关键是它能为我们带来什么样的好处。当我们逐渐按照DDD的方式来设计系统的时候,你会发现一切都很顺利而且很OO,我们完全可以使用OOA\OOD的方式且没有任何干扰的进行系统开发。
虽然说这是一个比较简单的在线考试系统,但是如果要把它做好其实是蛮庞大的,会和很多其他的系统联系,所以这里不会考虑太多的需求;
(这个项目本身的目的是为了演示DDD的设计、开发、架构,所以在需求上不会太难;)
1.2.1】用例分析通过用例来分析系统中的基本功能调用,这部分的调用是局限于外部的调用,毕竟是外部调用驱动内部调用;所有用例是捕获外部调用的功能图,具体的内部调用看具体的情况而定;
【学生用例】
块设备的驱动比字符设备的难,这是因为块设备的驱动和内核的联系进一步增大,但是同时块设备的访问的几个基本结构和字符还是有相似之处的。
有一句话必须记住:对于存储设备(硬盘~~带有机械的操作)而言,调整读写的顺序作用巨大,因为读写连续的扇区比分离的扇区快。
但是同时:SD卡和U盘这类设备没有机械上的限制,所以像上面说的进行连续扇区的调整显得就没有必要了。
先说一下对于硬盘这类设备的简单的驱动。
在linux的内核中,使用gendisk结构来表示一个独立的磁盘设备或者分区。这个结构中包含了磁盘的主设备号,次设备号以及设备名称。
在国嵌给的历程中,对gendisk这个结构体的填充是在simp_blkdev_init函数中完成的。在对gendisk这个结构填充之前要对其进行分配空间。具体代码如下:
simp_blkdev_disk = alloc_disk(1); if (!simp_blkdev_disk) { ret = -ENOMEM; goto err_alloc_disk; }
这里的alloc_disk函数是在内核中实现的,它后面的参数1代表的是使用次设备号的数量,这个数量是不能被修改的。
在分配好了关于gendisk的空间以后就开始对gendisk里面的成员进行填充。具体代码如下:
strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME); //宏定义simp_blkdev simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR; //主设备号 simp_blkdev_disk->first_minor = 0; //次设备号 simp_blkdev_disk->fops = &simp_blkdev_fops; //主要结构 simp_blkdev_disk->queue = simp_blkdev_queue; set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9); //宏定义(16*1024*1024),实际上就是这个结构体。
在填充好gendisk这个结构以后向内核中 注册这个磁盘设备。具体代码如下:
add_disk(simp_blkdev_disk);
在LDD中说,想内核中注册设备的必须在gendisk这个结构体已经填充好了以后,我们以前的字符设备的时候也是这么做的,不知道为什么LDD在这里强调了这个。
当不需要一个磁盘的时候要释放gendisk,释放部分的代码在函数simp_blkdev_exit中实现的。具体的释放代码如下:
del_gendisk(simp_blkdev_disk);
在simp_blkdev_exit中同时还有put_disk(simp_blkdev_disk),这个是用来进行操作gendisk的引用计数。simp_blkdev_exit还实现了blk_cleanup_queue清除请求队列的这个函数。终于说到请求队列了。
在说等待队列之前先要明确几个概念:
①用户希望对硬盘数据做的事情叫做请求,这个请求和IO请求是一样的,所以IO请求来自于上层。
②每一个IO请求对应内核中的一个bio结构。
③IO调度算法可以将连续的bio(也就是用户的对硬盘数据的相邻簇的请求)合并成一个request。
④多个request就是一个请求队列,这个请求队列的作用就是驱动程序响应用户的需求的队列。
请求队列在国嵌的程序中的simp_blkdev_queue
下面先说一下硬盘这类带有机械的存储设备的驱动。
这类驱动中用户的IO请求对应于硬盘上的簇可能是连续的,可能是不连续的,连续的当然好,如果要是不连续的,那么IO调度器就会对这些BIO进行排序(例如老谢说的电梯调度算法),合并成一个request,然后再接收请求,再合并成一个request,多个request之后那么我们的请求队列就形成了,然后就可以向驱动程序提交了。
在硬盘这类的存储设备中,请求队列的初始化代码如下:
simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
老谢说,在这种情况下首先调用的是内核中的make_requst函数,然后再调用自己定义的simp_blkdev_do_request。追了一下内核代码,会发现make_requst内核代码如下所示:
static int make_request(struct request_queue *q, struct bio * bio)
具体的就不贴了,不过可以知道这个make_request的作用就是使用IO调度器对多个bio的访问顺序进行了优化调整合并为一个request。也就是在执行完成了这个函数之后才去正式的执行内核的请求队列。
合并后的request其实还是一个结构,这个结构用来表征IO的请求,这个结构在内核中有具体的定义。
请求是一个结构,同时请求队列也是一个结构,这个请求队列在内核中的结构定义如下:
struct request_queue { /* * Together with queue_head for cacheline sharing */ struct list_head queue_head; struct request *last_merge; struct elevator_queue *elevator; /* * the queue request freelist, one for reads and one for writes */ struct request_list rq; request_fn_proc *request_fn; make_request_fn *make_request_fn; prep_rq_fn *prep_rq_fn; unplug_fn *unplug_fn; merge_bvec_fn *merge_bvec_fn; prepare_flush_fn *prepare_flush_fn; softirq_done_fn *softirq_done_fn; rq_timed_out_fn *rq_timed_out_fn; dma_drain_needed_fn *dma_drain_needed; lld_busy_fn *lld_busy_fn; /* * Dispatch queue sorting */ sector_t end_sector; struct request *boundary_rq; /* * Auto-unplugging state */ struct timer_list unplug_timer; int unplug_thresh; /* After this many requests */ unsigned long unplug_delay; /* After this many jiffies */ struct work_struct unplug_work; struct backing_dev_info backing_dev_info; /* * The queue owner gets to use this for whatever they like. * ll_rw_blk doesn't touch it. */ void *queuedata; /* * queue needs bounce pages for pages above this limit */ gfp_t bounce_gfp; /* * various queue flags, see QUEUE_* below */ unsigned long queue_flags; /* * protects queue structures from reentrancy. ->__queue_lock should * _never_ be used directly, it is queue private. always use * ->queue_lock. */ spinlock_t __queue_lock; spinlock_t *queue_lock; /* * queue kobject */ struct kobject kobj; /* * queue settings */ unsigned long nr_requests; /* Max # of requests */ unsigned int nr_congestion_on; unsigned int nr_congestion_off; unsigned int nr_batching; void *dma_drain_buffer; unsigned int dma_drain_size; unsigned int dma_pad_mask; unsigned int dma_alignment; struct blk_queue_tag *queue_tags; struct list_head tag_busy_list; unsigned int nr_sorted; unsigned int in_flight[2]; unsigned int rq_timeout; struct timer_list timeout; struct list_head timeout_list; struct queue_limits limits; /* * sg stuff */ unsigned int sg_timeout; unsigned int sg_reserved_size; int node; #ifdef CONFIG_BLK_DEV_IO_TRACE struct blk_trace *blk_trace; #endif /* * reserved for flush operations */ unsigned int ordered, next_ordered, ordseq; int orderr, ordcolor; struct request pre_flush_rq, bar_rq, post_flush_rq; struct request *orig_bar_rq; struct mutex sysfs_lock; #if defined(CONFIG_BLK_DEV_BSG) struct bsg_class_device bsg_dev; #endif };
LDD说,请求队列实现了一个插入接口,这个接口允许使用多个IO调度器,大部分IO调度器批量累计IO请求,并将它们排列为递增或者递减的顺序提交给驱动。
多个连续的bio会合并成为一个request,多个request就成为了一个请求队列,这样bio的是直接的也是最基本的请求,bio这个结构的定义如下:
struct bio { sector_t bi_sector; struct bio *bi_next; /* request queue link */ struct block_device *bi_bdev; /* target device */ unsigned long bi_flags; /* status, command, etc */ unsigned long bi_rw; /* low bits: r/w, high: priority */ unsigned int bi_vcnt; /* how may bio_vec's */ unsigned int bi_idx; /* current index into bio_vec array */ unsigned int bi_size; /* total size in bytes */ unsigned short bi_phys_segments; /* segments after physaddr coalesce*/ unsigned short bi_hw_segments; /* segments after DMA remapping */ unsigned int bi_max; /* max bio_vecs we can hold used as index into pool */ struct bio_vec *bi_io_vec; /* the actual vec list */ bio_end_io_t *bi_end_io; /* bi_end_io (bio) */ atomic_t bi_cnt; /* pin count: free when it hits zero */ void *bi_private; bio_destructor_t *bi_destructor; /* bi_destructor (bio) */ };
需要注意的是,在bio这个结构中最重要的
来北邮读研已经一学年了,导师给的研究方向是短波无线电台。研一阶段就给了做项目的前期准备,通过一个小项目做了一些预热,
不过麻雀虽小,五脏俱全。使用到了调制解调系统中会用到的方方面面。
在这过程中,也经过了很多的技术难关。
FPGA部分:
第一关:pi/4-DQPSK。关于如何选择合适的调制解调方式,首先能够在FPGA内部很好的实现,其次还得满足通信系统的速率要求,
基于综合考量。选择QPSK实现。同时为了减小载波相位对解调的影响。选择差分方式。并且pi/4-DQPSK峰均比比较合适。然后的问题
就是决定基带解调还是频带解调了?考虑到电话线路的恒定性,选择基带差分可以增大一部分解调增益。
第二关:成型滤波器。成型滤波器的作用是是的接收端能以最大性噪比输出。同时也是为了能够让信号保持在有效带宽内。成型滤波器
的设计借助了matlab的FDAtool工具,通过生成系数,导入到之前编好的滤波器模块内。
第三关:帧同步。同步是通信系统的核心问题,做不好,可能导致整个系统的奔溃。这里简单的选择了PN序列作为同步头,并且通过
检测相关峰实现最后的同步。同步以后根据过采样的特性,选择最佳采样区间。
第四关:维特比译码。在信号传输过程中,会受到一些干扰(随机噪声干扰,线路上的干扰,码间干扰等),因此必然导致传输出现
差错,选择信道编码也是必要的措施。这里选择了最简单的(2,1,2)维特比译码。
第五关:Modem控制芯片si3056的初始化和控制。Si3056有100多个寄存器,而且都是通过类似SPI的方式初始化的,这个过程看懂
pdf文档,比较好办。至于对其控制,对于FPGA来说就显得比较麻烦。因为控制过程都是串行化结构处理的,比较适合单片机等MCU处理,
FPGA只能通过状态机模拟这个串行化结构。
第六关:AT命令集。同样的要在FPGA内部实现AT命令集显得比较的啰嗦。本来也是适合MCU处理的,因此FPGA内部也是通过状态机
模拟这个串行化过程。状态机非常之庞大。分好几级:识别命令,处理命令,返回命令。
第七关:自适应均衡。这个还在研究阶段,也是这个系统比较重要的一个模块,没有他,只要话路一长,存在严重的码间干扰,误码率
同步率严重恶化。拟采用LMS实现自适应均衡。
PC部分:
第一关:串口。这个比较容易。通过Python很快就调试完毕了。而且FPGA内部程序也不复杂。
第二关:上位机。这部分内容主要实现AT命令集的发生,并且调度整个系统。也是由Python来写的。因为自己在学Python。
项目介绍: