好久没更新了,主要是最近在准备教师资格证面试,然后还在写毕业设计和毕业论文,所以就没太注意文章输出了。今天这篇文章摘自我毕业论文中的专业综述,略有修改,隐去了专业的隐私信息。主要是我对大学所学知识的总结

从我的角度来看,我对于整个大学所学到的知识分为三个部分,第一部分是专业能力,第二部分是沟通能力,第三部分是学习能力。下面我将分段落来阐述这三大部分。

一.专业能力

专业能力由里及表,我主要把其分为五个子模块,分别是硬件、操作系统、编程语言、程序和业务。不过需要强调的是,这个由表及里只是狭义上的,因为我们无法断定操作系统和编程语言哪个在里面,哪个在表面。我想,我这个划分更适合按照程序员接触的角度来说明。

这几个模块只针对于我自己的专业,根据我自己的理解划分的

1. 硬件

对于硬件来说,它主要分为CPU、GPU、内存、磁盘、各种IO(如网络IO)、总线和主板等等。硬件的组合成为了物理层面上的服务器,每个硬件厂商都有对应的特点,譬如对于CPU来说,有ARM架构和X86等架构。

CPU和GPU有什么不同呢?在我看来,CPU更适合通用计算,且CPU因为需要分支跳转和预测,逻辑比较复杂。GPU更适合大规模并发运算。从组成上来说,CPU有较大的cache,而GPU有更多的ALU。

我们常常把寄存器、内存和磁盘放到一起,用来对比他们的速度,我们知道内存比磁盘快得多,同时寄存器比内存又快的多。对于磁盘来说,之前的机械硬盘比较慢是因为它需要机械的磁头,而目前比较常用的已经变成了固态硬盘。因为速度不同,我们平时所谓的高性能其实就是通过不同的策略来提高寄存器的利用率。

2. 操作系统

按照《现代操作系统》的定义,操作系统有两个功能,一个是为用户程序抽象,方便程序员编程,另一个是管理计算机资源,高效利用硬件。随着时代的进步,目前的操作系统主要是多核且有逐步虚拟化和上云的趋势,不过我对这些了解不多,只是知道docker,k8s等皮毛,所以暂且不表。

对于操作系统来说,从PC上来说,分为System/360,Windows和Unix等等,我们说的Linux,FreeBSD等系统都是基于贝尔实验室的Unix的。由于现在大部分服务均运行在Linux上,并且大学讲的也是Linux,所以本模块主要阐述Linux。

对于Linux来说,它从结构上来说,主要分为Shell/GUI、Kernel两层。不过严格上来说,shell/GUI其实并不属于操作系统,它只是用来和用户交互罢了。用户程序运行在用户态,内核负责将用户程序和硬件进行隔离,所有的程序在访问内存,硬件和网络资源的时候都需要切换到内核态,以保证系统的安全性和稳定性。

从模块上来分,操作系统主要包括进程管理、内存管理、文件管理和IO管理。资源管理主要是和CPU相关的调度问题,如进程调度、线程调度和协程调度。内存管理主要是和内存有关的一些问题,如分页式,分段式,段页结合式,虚拟内存等。IO管理主要是网络相关的IO问题,如零拷贝、DMA、阻塞非阻塞、同步异步等等。

对于进程线程管理来说,我们要从历史的角度来分析问题。我们常说进程是资源管理的基本单位,线程是资源调度的基本单位。进程是OS对程序,内存和计算资源的抽象,每个进程都有自己的数据段,程序段和堆栈段。进程分为运行,阻塞和就绪三种状态,阻塞分为IO阻塞和锁阻塞,进程在阻塞状态下不能运行。OS通过进程的切换进而实现了伪并行。但是进程中含有这次任务的全部数据段和程序段,切换起来代价太大,同时,进程之间共享数据(如地址空间,句柄,信号等等)也比较麻烦,所以,线程就诞生了,线程切换要比进程快10-100倍,同时线程可以访问进程中的所有数据。当线程被IO阻塞的时候,OS就可以完成另一个线程的计算任务。有了线程就万事大吉了吗?并不是这样的,我们知道,线程的切换也需要从用户态陷入到内核态中,频繁的上下文切换也会导致CPU计算资源的浪费,所以我们就想假如我们实现用户态的线程,任务的切换只在用户态完成,这样不是又省了一段时间吗?这就是我们说的协程。但是协程也不是一劳永逸的,在面对阻塞式IO的时候,整个线程都会被挂起,协程也无从谈起。同时用户需要自定义协程调度方式,这也提高了其门槛。对于线程和同步来说,我们要知道唤醒,join,yield,加锁等概念,同时还要注意避免死锁,这其中有许多算法,如哲学家吃面,理发师问题,银行家问题等等。

对于内存管理来说,它通过地址空间的概念对存储器进行抽象,同时,通过交换技术和虚拟内存,使得磁盘置换也可以作为内存来使用。在虚拟内存技术中,MMU通过页表把程序产生的虚拟地址映射为真实的物理地址,当虚拟地址在页表中不存在时,则通过缺页中断对磁盘中的页面进行置换,其中,我们通过页表缓冲区来解决页表造成的多次内存访问问题,通过多级页表来加速分页过程。分页是面向存储器的一维的抽象,而分段则是面向用户的二维抽象,像我们平时说的数据段,程序段,堆栈段就是分段的体现。值得注意的是LRU这种页面置换算法在工程中经常用到。

我觉得存储系统抽象也是非常成功的,它成功地把0和1的字节抽象成普通用户可以理解的文件和文件夹,这不能不说是一项伟大且成功的尝试。OS和存储系统的交互可以归类到IO层面,我们聊存储时更倾向于它的文件系统。

对于IO来说,主要学习了操作系统的IO抽象,这里通过一个WEB服务器的场景(同步阻塞模型)来说明问题。假设此时用户需要从socket中读入一个输入流,首先进程会先从用户态陷入内核态,接着内核会阻塞监听SOCKET直至有网络连接的发生,然后内核告知DMA将网卡的数据读入内核空间的内存中,在这个过程中,OS虽然会重新调度其他线程进行工作,但是当前线程还是被阻塞的,当DMA完成工作后,会发起一个中断通知CPU数据拷贝完成,然后OS重新调度原来的线程,并将内核态中的数据拷贝到用户态中。之后进程重回用户态会进行自己的业务处理,然后会把数据持久化到硬盘里,在持久化的过程中,还是先将进程上下文由用户态陷入内核态,然后CPU将用户态的数据拷贝到内核态中,然后委托DMA将内核态的数据写入到硬盘中。在上面的例子中,我们知道了通过DMA可以减少CPU计算资源的浪费。同时,如果我们不进行业务处理,那么就可以将内核空间的数据通过mmap进行映射,使得拷贝的速度更快。我们再考虑一个问题,还是这个例子,假如说在这个阻塞IO处理的过程中,有其他网络连接进入服务器了,这时候该怎么办?没错,这时候我们只能去开多线程再接收另外的socket连接,这就是我们常说的BIO+多线程的服务器模型。但是我们不能对每个连接都开一个线程,这样线程会变得无限多导致内存资源浪费过多。所以OS就提供了非阻塞IO,所谓的非阻塞,就是当数据在从网卡到用户态buffer的两次拷贝中,用户线程不会被一直阻塞,而是去不断询问数据是否准备好。有了非阻塞IO之后,我们就可以通过一个线程专门去询问将已经accept的连接,如果该连接数据准备好,我们就进行相应连接的业务操作,这便是我们多路复用中的select。不过select中需要将用户态的accept数组拷贝到内核态的数组中,同时返回的只是可读fd的个数,性能依旧不高,所以在epoll中,它只是传入的accept数组中修改的部分而不是全部,同时通过异步事件唤醒, 内核仅会将有 IO 事件的文件描述符返回给用户。epoll大大提高了性能。

同时,在操作系统的这个层面,还要注意网络通信这个东东。正是因为有了计算机网络,我们世界各地的机器才能通过操作系统进行连接和通信。我们知道,目前的网络层次有两种标准,分别是OSI七层网络模型和TCP/IP四层网络模型。我们常用的一般有网络层的IP协议,运输层的TCP和UDP协议(此层有端口号),以及应用层的HTTP,FTP,WebSocket,DNS,SMTP等协议。网络通信主要学习的有TCP的三次握手四次挥手用来保证在网络不确定情况下可靠连接的建立。同时TCP中的拥塞控制,流量控制来保证发送端和接收端中数据的容量。

3. 编程语言

不包括汇编语言和机器语言

在我看来,编程语言是一种工具,没有好坏之分,无论他们的语法如何,它们最后都是调用了操作系统对外的API,从而驱使操作系统去访问硬件进行读取、存储、计算或者通信。

编程语言按照种类来分,大致可以分为面向对象、面向过程;静态语言、动态语言;编译型语言、解释型语言等等。因为这些语言的编译过程,运行环境,使用方式的不同,所以它们有了不同的场景:如JS适合前端开发,C系列适合底层和后台开发,Java适合后台开发,Python适合爬虫开发等等。但是我们始终要记住一个点,这些语言适合某一个领域,不意味着它只能用于和这个领域。譬如Java不仅可以写后台,Swing也可以写客户端,applet/jsp也可以写页面,就是这个道理。

这里把C和Java做一个对比:

对于C来说,程序员会以系统的视角去看内存——一块连续的字节数组。一个C程序最起码是一个进程,它会直接映射到操作系统对进程的空间分配中,如程序块和数据,用户栈,运行堆,共享库以及用户无法访问的内核虚拟内存。一个简单的HelloWorld.c程序,它的代码会存放到程序块区域,静态变量会存放到数据区,当调用函数时,会把函数地址和变量压入用户栈中,当使用malloc分配空间时,数据会存放在运行堆中。

对于Java来说,VM会对内存再进行一次抽象和封装。它把内存分为线程独有的程序计数器、虚拟机栈、本地方法栈和线程共享的堆以及方法区(其实方法区也是堆)。当程序new一个对象后,对象的实例会被存放到堆中,对象的信息和方法以及静态变量会被存放到方法区中。

总结来说,C语言中,程序员可以访问的内存单位是字节数组;而Java语言中,程序员可以访问的内存单位是抽象出来的对象。因为寄存器可以访问的内存单位也是字节数组,这说明使用C语言的程序员可以直接访问底层,进行更多操作,这也是C语言程序员鄙视Java程序员的一个重要原因之一。但是访问的更多不仅仅意味着权利,也以为着责任,因为没有对象级的抽象,导致了C程序尤其是大型程序会莫名奇妙的抛出缓冲区溢出,数组越界,乱码等等难以找到根源的问题。

对于C来说,程序员需要主动去申请和释放内存。而对于Java来说,因为有了VM的存在,GC会自动回收不可达的对象。但是同时GC线程在回收垃圾的时候会造成STW,拖慢程序的性能。而且,因为对象交给了VM来负责清除,那么对象的清除时间其实是不可预测的。相对来说,C程序员可以完全操控内存的分配和回收时间,可以完全掌握内存的信息,这点是Java程序员做不到的。但是,如果C程序中忘记free内存空间,也将会造成不可估量的损失。

从内存处理的方式来看,C程序有着不容置疑的对内存完全掌控的能力,而对Java程序来说,因为VM做了一层抽象后,Java程序把内存的操作交给了VM来处理,变成了间接访问。同时,因为有了VM,Java程序可以随心所欲的编写代码而不用担心出现很大的BUG,这导致了Java语言极易上手,Java程序员良莠不齐,一直被行业诟病。

(节选自https://wxxlamp.cn/2020/12/17/java-memory/)

不管是什么语言,我们都需要一些基础的语法,如变量定义,分支,循环,指针,引用等等。同时,我们还需要透过语言看内存,知道每种变量类型对应的位数。同时,还需了解8bits=1byte,1kb=1024bytes,位运算,补码反码,进制转换等基础知识。这里有个冷知识,为什么我们平常的程序用文本文件打开它会是cafe babe 0000 0037 0020 0a00 0400 180a这样的排列方式,我们可以发现它会是2bytes=16bits排列一起,原因是什么我暂时也不知道。

4. 程序

编程语言上层,就是程序了,用户通过各种编程语言编写各种类型的程序,我大概把他们分为以下几类:

  1. 基础类程序,如complier, linker, vm等等
  2. 中间件程序,如redis, mysql, k8s, docker等等
  3. 框架类程序,如spring, logicsvr, junit等
  4. 业务类程序,面向普通用户的程序,如淘宝,微信等等

对于程序来说,我们应该仔细了解它的开发流程和迭代方法:如需求评审、概要设计、详细设计(流程图,类图,设计模式)、代码编写、软件测试(单元测试,灰度测试,功能测试,链路测试)、系统构建和运维等等。

在这个层面中,最为重要的便是代码编写,BUG修复,架构设计的能力。在普通情况下,当一个校招生入职后,它不会说直接去做架构,搞高性能,高可用,可扩展,数据一致性等东西,他会从一个小兵做起,去拧一些螺丝,对一个需求在既有的框架和架构内进行CURD的填充工作。那么,在编程的过程中,我们要做的就是通过良好的编程习惯,写出不仅能让机器看得懂的程序,也写出让人看得懂的程序。写程序是一方面,跟程序配套的文档和注释也是很重要的一部分,文档反映了系统的迭代计划和迭代方向,也反映了系统的整体架构和业务模型,是大型系统开发中必备的一项工作。

在我们写程序的过程中,总是离不开两个话题,一个是计算,一个是存储。要知道,计算和存储其实就代表了时间和空间,而根据现在的技术和实际经验表明,时间和空间不能同时满足的。一个系统,不可能既运行速度快,又只占用少量的存储。换句话说,一个问题的解决方式,要么是时间换空间,要么是空间换时间。这两种方式,没有对错之分,只有选择合适的,才是最好的。由于当下硬件对程序的限制不是很大,所以很多系统都是通过空间换时间的方式来提高系统的效率,譬如我们的多线程,线程池,缓存等技术都是空间换时间。其实空间和时间除了映射着存储和计算之外,面向对象语言类中的字段就代表的是数据结构,也就是空间,类中的方法就代表的是算法,也就是时间。当一个程序真正运行的时候,类中的字段会存储到内存中,而方法则是会通过CPU进行计算。

数据结构和算法对应着空间和时间,我们学习了堆,栈,队列,树,图,数组,链表等数据结构,这些数据结构全都是对内存的抽象,方便了用户进行编程。而算法我们也学习了贪心、分治、动态规划、回溯、分支限界五种基本方法,同时也有一些应用类算法如排序,哈夫曼树,深搜广搜等等。

除了代码代码的编写,我们还应该掌握系统的设计,在我看来,系统设计的好坏有四个衡量标准:性能、可用、效率、安全。

所谓性能,指的是系统能承担的计算和IO量,我们可以通过TPS等指标去衡量,常见的提高性能的思路有集群,缓存,异步IO,批处理,池化技术,多线程,乐观锁等方式。但是一项技术有优点就有缺点,比较常见的就是缓存一致性怎么保证,异步消息怎么确保消费,集群无状态怎么设置,线程安全怎么解决,池化技术如何保证上下文无关等等。

所谓可用,指的是系统正常服务的时间,像我们常说的5个9或者99%等就是用来形容系统的可用程度。业界常见的做法就是通过多副本,压缩,限流,及时熔断等方式来提高系统的可用性,像我们常说的Redis哨兵集群就是通过多副本保证可用性的。同时既然有多副本,我们就必须保证数据的强一致或者base,目前常见的算法是paxos,这个微信的存储一直用的paxosstore,反馈也挺不错的。

所谓效率,更倾向于业务的架构和需求迭代的流程。在多数业务中,我们需要不断试错,快速上线,这个时候就需要我们尽快的开发程序。此时,我们需要合适的框架如Spring全家桶等,也需要正规的迭代流程,如我们最近常说的敏捷开发等,还需要合适的业务划分,如DDD,微服务等这样的开发方式。

所谓安全,指的是系统保证业务流程的逻辑安全和用户安全。系统需要防止SQL注入,XSS,DDOS,CSXF,ARP伪装等web攻击方式

5. 业务

一个好的开发人员,也一定是一个懂业务的人员。

每个计算机从业者都想去写强技术弱业务的程序。譬如,很多人都想写中间件,如缓存、通信、池化、异步等程序。因为这样的工作可以提高开发人员的技术,但是很多人都没有这个机会,一方面是公司的岗位少,另一方面是开发人员的技术不够精湛。

当开发人员明白了业务的性质,就可以慢慢懂得业务的迭代方向,然后就可以设计出好的系统架构来提高开发效率。技术人员只有了解了业务之后,才有可能对产品经理和运营人员提出的需求进行质疑,进而反哺于业务生态产生商业价值。

对于从事软件工程专业的人员来说,除了掌握技术上的能力之外,业务能力也是必不可少的。金融业的软件开发人员需要熟悉金融知识,传统行业的软件开发人员需要熟悉对应的知识。我之前经常看到各个公司各个行业的JD,发现几乎每个招聘下面都有一条:有相关从业经验者优先。这足以说明业务的重要性。

二. 沟通能力

无论是和朋友的交流,还是和同学老师的交流,甚至与和公司同事老板的交流,都需要用到沟通能力。通过和老师的沟通,掌握了知识;通过和同学的沟通,适应了环境;通过和朋友的沟通,提升了自我。

工作上的沟通包括和产品经理的沟通,方便对齐目标。也包括跟开发同事沟通,方便协调进度。还包括跟老板的沟通,方便同步项目。如果和产品经理的沟通出现问题,项目就可能会返工甚至导致开发时间的增加进而延误了项目的上线时间;如果和开发同事沟通出现问题,就可能导致开发进度不一致甚至会导致开发的系统产生漏洞;如果和老板沟通出现问题,就可能导致自己和老板的目标不一致间接影响年终考核和绩效考评。

沟通一定要具有主动性,主动性意味着owner意识,或者说是责任心。要真正的对自己的项目和事情负责,主动push和跟进,才能对项目了然于胸。

更进一步,还需要具备和社会,和环境沟通的能力,我们要通过沟通去获得我们需要的信息,打破信息壁垒,更好的迭代自我。其实,沟通不仅仅指的是说话,宽泛来讲,沟通更是一种人际交往能力,沟通能力虽然没有专业知识那么硬性,但是在工作之后也会对我们产生决定性影响。

三. 学习能力

我认为的学习能力,不是指那种填鸭式的、死记硬背的学习,而是一种周期性的,逐渐递进的一种过程。我把学习分为三步,分别是学习,理解和运用

学习需要带有一定的动机和需求,当学生对某种事物感兴趣的时候,他才会具备好奇心和自驱力,才能主动的吸收知识,进行思考。同时,在学习的过程中,需要对学习的知识进行总结和发现,发现学习的知识和现有的知识有何区别和不同,进而加深自己的理解,锻炼自己举一反三的能力。学习之后,不能把知识干晾着,还需要通过其他东西对学习的成果进行检验,把学习到的知识运用到其他方面的流程中,产生价值,才算是整个学习流程的结束。

譬如现在很多人都会看源码,如Spring,Mybatis等等,但是其中大家都觉得看完就忘,甚至觉得没有提升。这是因为很多人就是单纯的看源码,遇到不懂的就跳过,看完之后是一种似懂非懂的感觉。在我看来这都是大家没有掌握学习的方法。首先,我们一定要知道我们看源码的目的,在我之前看mybatis源码的时候,是因为我当时遇到了一个映射需求,想看看mybatis的实现方式,有了目的就有看下去的动力了,这就是我们说的带着问题学习。然后看完后还要总结和抽象,获得自己问题的解决方案,同时还要总结出其他比较优秀的地方。然后最后一步,自己写一个demo,在实际场景中去运用自己学到的知识对问题进行解决。

四. 后记

知道的越多,才知道自己知道的越少。上面的记录是我对整个计算机全局的认识,可能有一些问题,欢迎大家指正。

路漫漫兮其修远兮,吾将上下而求索。