NB的架构师都要具备足够的技术深度,然后才能透过问题看本质,所谓技术深度就是扎实的基础知识,要不断深入,反复研究学习,打磨自己的硬实力。不论从事云计算、虚拟化、容器、大数据、人工智能,几乎都是基于 Linux 服务器部署服务。 一、linux系统结构 linux系统看似纷繁复杂,单其核心只有一点,那就冯洛伊曼体系“存储计算”,万变不离其宗。就像一颗大树一样,枝叶繁多,但主干却很清晰简单。 Linux系统一般有4个主要部分:programs/utilities/tools 内核、shell/工具(GUN工具)、文件系统和应用程序。内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统。部分层次结构如图1-1所示。 Computer Resources:硬件资源 Kernel:内核 GUN工具: Shell:shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行,是一个命令解释器 Programs/Utilities/Tools:库函数、工具等 File systems:文件系统是文件存放在磁盘等存储设备上的组织方法。Linux系统能支持多种目前流行的文件系统,如EXT2、 EXT3、 FAT、 FAT32、 VFAT和ISO9660。 User Application:Linux应用,标准的Linux系统一般都有一套被称为应用程序的程序集,它包括文本编辑器、编程语言、X Window、办公套件、Internet工具和数据库等 Linux开机后,内核启动,激活内核空间,抽象硬件、初始化硬件参数等,运行并维护虚拟内存、调度器、信号及进程间通信(IPC)。内核启动后,再加载Shell和用户应用程序,用户应用程序使用C\C++编写,被编译成机器码,形成一个进程,通过系统调用(Syscall)与内核系统进行联通。进程间交流需要使用特殊的进程间通信(IPC)机制。 二. Linux系统1: linux内核组成 Linux内核是世界上最大的开源项目之一,内核是与计算机硬件接口的易替换软件的最低级别。它负责将所有以“用户模式”运行的应用程序连接到物理硬件,并允许称为服务器的进程使用进程间通信(IPC)彼此获取信息。 内核是操作系统的核心,具有很多最基本功能,其核心功能就是:管理硬件设备,供应用程序使用。硬件设备包括CPU、Memory(内存和外存)、输入输出设备、网络设备和其它的外围设备。它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性。 Linux 内核由如下几部分组成:内存管理、进程管理、设备驱动程序、文件系统和网络管理等。如图: 系统调用接口: SCI 层提供了某些机制执行从用户空间到内核的函数调用。这个接口依赖于体系结构,甚至在相同的处理器家族内也是如此。SCI 实际上是一个非常有用的函数调用多路复用和多路分解服务。在 ./linux/kernel 中您可以找到 SCI 的实现,并在 ./linux/arch 中找到依赖于体系结构的部分。 1、内存管理 负责管理Memory(内存)资源,以便让各个进程可以安全地共享机器的内存资源。另外,内存管理会提供虚拟内存的机制,该机制可以让进程使用多于系统可用Memory的内存,不用的内存会通过文件系统保存在外部非易失存储器中,需要使用的时候,再取回到内存中。 内存管理系统在操作系统中,不同的进程有不同的内存空间,这些内存空间需要统一的管理和分配,这就需要内存管理系统。 对任何一台计算机而言,其内存以及其它资源都是有限的。为了让有限的物理内存满足应用程序对内存的大需求量,Linux 采用了称为“虚拟内存”的内存管理方式。Linux 将内存划分为容易处理的“内存页”(对于大部分体系结构来说都是 4KB)。Linux 包括了管理可用内存的方式,以及物理和虚拟映射所使用的硬件机制。 不过内存管理要管理的可不止 4KB 缓冲区。Linux 提供了对 4KB 缓冲区的抽象,例如 slab 分配器。这种内存管理模式使用 4KB 缓冲区为基数,然后从中分配结构,并跟踪内存页使用情况,比如哪些内存页是满的,哪些页面没有完全使用,哪些页面为空。这样就允许该模式根据系统需要来动态调整内存使用。 为了支持多个用户使用内存,有时会出现可用内存被消耗光的情况。由于这个原因,页面可以移出内存并放入磁盘中。这个过程称为交换,因为页面会被从内存交换到硬盘上。内存管理的源代码可以在 ./linux/mm 中找到。 2 .进程管理 进程管理系统在操作系统中,进程的执行也需要分配CPU来执行,所以,为了管理进程,我们还需要一个进程管理系统。如果运行的进程很多,则一个CPU会并发地运行多个进程,这就需要CPU的调度能力了。 进程实际是某特定应用程序的一个运行实体。在 Linux 系统中,能够同时运行多个进程,Linux 通过在短的时间间隔内轮流运行这些进程而实现“多任务”。这一短的时间间隔称为“时间片”,让进程轮流运行的方法称为“进程调度” ,完成调度的程序称为调度程序。 进程调度控制进程对CPU的访问。当需要选择下一个进程运行时,由调度程序选择最值得运行的进程。可运行进程实际上是仅等待CPU资源的进程,如果某个进程在等待其它资源,则该进程是不可运行进程。Linux使用了比较简单的基于优先级的进程调度算法选择新的进程。 通过多任务机制,每个进程可认为只有自己独占计算机,从而简化程序的编写。每个进程有自己单独的地址空间,并且只能由这一进程访问,这样,操作系统避免了进程之间的互相干扰以及“坏”程序对系统可能造成的危害。为了完成某特定任务,有时需要综合两个程序的功能,例如一个程序输出文本,而另一个程序对文本进行排序。为此,操作系统还提供进程间的通讯机制来帮助完成这样的任务。Linux 中常见的进程间通讯机制有信号、管道、共享内存、信号量和套接字等。 内核通过 SCI 提供了一个应用程序编程接口(API)来创建一个新进程(fork、exec 或 Portable Operating System Interface [POSⅨ] 函数),停止进程(kill、exit),并在它们之间进行通信和同步(signal 或者 POSⅨ 机制)。 3. 文件系统 Linux系统,一切皆文件: 1)启动一个进程,需要一个二进制文件。 2)启动进程时,需要加载一些配置文件如yml, properties等,这是文本文件 3)把日志打印到控制台上,是标准输出stdout文件 4)一个进程的输出作为另一个进程的输入,称为管道,管道也是一个文件 5)进程可以通过网络和其他进程通信,建立的socket,也是一个文件 6)进程需要访问的外部设备,也是一个文件 7)文件夹也是一个文件每个文件,Linux都会分配一个文件描述符(File Descriptor),这是一个整数。有了这个文件描述符,我们就可以使用系统调用,查看或干预进程运行的方方面面。 和 DOS 等操作系统不同,Linux 操作系统中单独的文件系统并不是由驱动器号或驱动器名称(如 A: 或 C: 等)来标识的。相反,和 UNIX 操作系统一样,Linux 操作系统将独立的文件系统组合成了一个层次化的树形结构,并且由一个单独的实体代表这一文件系统。Linux 将新的文件系统通过一个称为“挂装”或“挂上”的操作将其挂装到某个目录上,从而让不同的文件系统结合成为一个整体。Linux 操作系统的一个重要特点是它支持许多不同类型的文件系统。Linux 中最普遍使用的文件系统是 Ext2,它也是 Linux 土生土长的文件系统。但 Linux 也能够支持 FAT、VFAT、FAT32、MINIX 等不同类型的文件系统,从而可以方便地和其它操作系统交换数据。由于 Linux 支持许多不同的文件系统,并且将它们组织成了一个统一的虚拟文件系统. 虚拟文件系统(VirtualFileSystem,VFS):隐藏了各种硬件的具体细节,把文件系统操作和不同文件系统的具体实现细节分离了开来,为所有的设备提供了统一的接口,VFS提供了多达数十种不同的文件系统。虚拟文件系统可以分为逻辑文件系统和设备驱动程序。逻辑文件系统指Linux所支持的文件系统,如ext2,fat等,设备驱动程序指为每一种硬件控制器所编写的设备驱动程序模块。 虚拟文件系统(VFS)是 Linux 内核中非常有用的一个方面,因为它为文件系统提供了一个通用的接口抽象。VFS 在 SCI 和内核所支持的文件系统之间提供了一个交换层。即VFS 在用户和文件系统之间提供了一个交换层。 VFS 在用户和文件系统之间提供了一个交换层: 在 VFS 上面,是对诸如 open、close、read 和 write 之类的函数的一个通用 API 抽象。在 VFS 下面是文件系统抽象,它定义了上层函数的实现方式。它们是给定文件系统(超过 50 个)的插件。文件系统的源代码可以在 ./linux/fs 中找到。 文件系统层之下是缓冲区缓存,它为文件系统层提供了一个通用函数集(与具体文件系统无关)。这个缓存层通过将数据保留一段时间(或者随即预先读取数据以便在需要是就可用)优化了对物理设备的访问。缓冲区缓存之下是设备驱动程序,它实现了特定物理设备的接口。 因此,用户和进程不需要知道文件所在的文件系统类型,而只需要象使用 Ext2 文件系统中的文件一样使用它们。 4. 设备驱动程序 设备驱动程序是 Linux 内核的主要部分。和操作系统的其它部分类似,设备驱动程序运行在高特权级的处理器环境中,从而可以直接对硬件进行操作,但正因为如此,任何一个设备驱动程序的错误都可能导致操作系统的崩溃。设备驱动程序实际控制操作系统和硬件设备之间的交互。设备驱动程序提供一组操作系统可理解的抽象接口完成和操作系统之间的交互,而与硬件相关的具体操作细节由设备驱动程序完成。一般而言,设备驱动程序和设备的控制芯片有关,例如,如果计算机硬盘是 SCSI 硬盘,则需要使用 SCSI 驱动程序,而不是 IDE 驱动程序。 5.网络接口(NET) 提供了对各种网络标准的存取和各种网络硬件的支持。网络接口可分为网络协议和网络驱动程序。网络协议部分负责实现每一种可能的网络传输协议。众所周知,TCP/IP 协议是 Internet 的标准协议,同时也是事实上的工业标准。Linux 的网络实现支持 BSD 套接字,支持全部的TCP/IP协议。Linux内核的网络部分由BSD套接字、网络协议层和网络设备驱动程序组成。 网络设备驱动程序负责与硬件设备通讯,每一种可能的硬件设备都有相应的设备驱动程序。 需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享 二、Linux系统1: 内核运行原理 百度搜索:漫画趣解Linux内核:内容大概如下: 一幅来自 TurnOff.us 的漫画 “InSide The Linux Kernel” 。TurnOff.us 是一个极客漫画网站,作者 Daniel Stori 画了一些非常有趣的关于编程语言、Web、云计算、Linux 相关的漫画。 整体架构画面分为三层:(按我个人理解) 最下面一层看起来像是一个仓库,就是文件系统(File System); 地面层有很多人(大部分都是企鹅)在各种忙碌,代表着进程表(Process Table); 还有一个跃层,代表着人机交互界面(Terminals And Termina process); 这些就是Linux内核的基本层次架构,所有程序执行、资源调度、系统功能都在这个内核中运行和实现。 关于操作系统处理任务/服务的大体流程简单归纳为:1.输入—2.中断/异常处理——3.调度进程——4.服务进程处理——5.输出结果: 接下来我们按照漫画的分层分开说明一下这些流程: 1、顶层的半层:输入输出交互界面 交互界面是一个跃层,说明它是内核的一部分,但和进程管理大厅联系紧密。这里墙上有很多屏幕,代表它通过可视化的界面来处理信息,实际上就是系统和人的交互界面。 屏幕有很多个,代表系统可以同时处理多路的命令交互。屏幕上的内容都有不同,有的屏幕还没有开启,有的屏幕是字符界面,有的还有图形界面,代表不同的GUI的交互状态。 这里有两个进程来管理这些屏幕。有一个进程面向屏幕,正在一个控制板上进行操作,笔者的理解是它是一个输出进程,可以控制各个屏幕的不同状态和输出内容;另一个进程面向大厅,手中拿着一份文件,应该正在向大厅里面的进程下达质量,所以它是一个输入控制进程。这样我们也能够理解为什么这里是一个半层,因为它需要一个开放的架构来连接输入输出和执行进程。 跃层有很多不同的屏幕,每个屏幕上写着 TTY(这就是对外的终端)。比如说最左边 tty4 上输入了 “fre” ——这是想输入 “freshmeat…” 么 :d ;它旁边的 tty2 和 tty3 就正常多了,看起来是比较正常的命令;tty7 显示的图形界面,对,图形界面(X Window)一般就在 7 号终端;tty5 和 tty6 是空的,这表示这两个终端没人用。等等,tty1 呢? 跃 层,也是最接近用户的一层。两只企鹅在名为 TTY 的窗口面前工作,一只企鹅在控制台前戳戳点点,另一只在仔细端详程序的输出。TTY 中文为电传打字机,关于 TTY,可以追溯到计算机的远古时代,那时候我们使用的还只是没有主机的打字机。设备的输入要经过长长的串行线路才能到达那昂贵的大型主机 (Mainframe Computer)。 作为 Unix-like 的 Linux 也继承了这一特性,在 /dev 目录下和 ps 命令的输出中我们都可以看到它的身影。 TTY(终端)是对外沟通的渠道之一,但是,不是每一个进程都需要 tty,某些进程可以直接通过其他途径(比如端口)来和外部进行通信,对外提供服务的,所以,这一层不是完整的一层,只是个跃层。 2、中间层:进程和进程调度 中间层则体现的是流程中的:调度,服务以及输出, 进程调度处理任务:忙碌的大厅,各司其职。 大厅里,就是各种进程运行和忙碌的主要场所了。 大厅的中心,是一张长条桌。这里应该就是主进程表了,一个主进程正在和一堆进程开会,可能正在协调它们的运行;这些进程神态和状态各异,有的认真听讲,有的不屑一顾,有的左顾右盼,有的沉默不语...,确实很像系统中各种任务执行的状态。 大厅一角堆了很多管道,这是Linux处理信息的 应用进程(企鹅): 图中有很多企鹅,它们通常代表着在Linux内核中运行的进程。之所以使用企鹅,应该是因为Linux的Logo和形象代表就是一个企鹅。但如果我们认真观察,会发现虽然它们都是企鹅,但是它们的装扮、动作甚至神态都是不一样的,隐喻着它们有不同的特性和状态。一般情况下,企鹅的胸前会挂着一个写有数字的工牌,代表着这个进程的进程编号。 初始化(init)进程:左上角有一个小企鹅,站着,仿佛在说些什么这显然是一位家长式的人物,不过看起来周围坐的那些小企鹅不是很听话 —— 你看有好多走神、自顾自聊天的 —— “喂喂,说你呢,哇塞娃(171),转过身来”。它代表着 Linux 内核中的初始化(init)进程,也就是我们常说的 PID 为 1 的进程。桌子上坐的小企鹅都在等待状态(Wait)中,等待工作任务。 监控进程(看门狗):除了企鹅之外,我们还看到有几条小狗。这些都是看门狗(Watch Dog)。它们负责在系统内部进行巡查,处理各种异常状况。当小企鹅们不听话时,它就会汪汪地叫喊起来。 异常进程: 最后还有一个小丑,应该就代表着系统内部的异常进程,比如病毒木马等恶意软件。 cron 进程:看它急得头上都冒汗了,这位老弟不断的看着手表,执行着周期性任务。 web守护进程:一只 PID 为 1341 的小企鹅就是大名鼎鼎的 Apache HTTP 服务器进程。它坚守在 80 端口提供 HTTP 服务。它头上的羽毛就是 Apache 的标志。 ssh守护进程:墨镜的企鹅守护着 22 端口。它看着要比其他的企鹅要更加有威严,脸上彷佛写着生人勿进四个字。原来它看护的是用于 SSH 服务的 22 端口,SSH 服务常常用于远程登陆,所以必须要仔细审查。 进程交互:管道 两位企鹅累的满头大汗,任劳任怨的在搬动着管道。一只小企鹅可以把自己手上的东西通过这个管道,传递给后面的小企鹅。 进程端口号:门 这个大厅里面,有几扇门,代表着和外部世界沟通的网络端口。 通用web端口号80: 左边的那个门上面的编号是80,这是标准的HTTP的端口号,所以这是一个标准Web端口;端口旁边有一个守卫进程,熟悉网络编程的同学应该可以看出来它就是Apache Tomcat,因为它头上戴着那个熟悉的羽毛。 ssh端口号22:右边那个门编号是22,就是SSH的端口,旁边那个戒备森严,戴墨镜耳机的保安,表明了这个端口的安全级别比较高。 ftp端口21:中间角落里面那个门的门牌都歪了,因为它已经年久失修。因为它是ftp端口(端口编号21),现在已经基本上没人用了。 系统交互:楼梯 和shell界面交互:SSH旁边的楼梯,可以上到交互界面层; 和文件系统交互:而21端口旁边,还有一个隐蔽的楼梯,上面FS的指示牌,表明从这里可以下到文件系统层。 3、地基层:文件系统 文件系统是单独的一层。这里面有很多柜子,按照行列码放整齐,表明了文件系统保存文件的方式和结构。文件放在柜子的抽屉里面,而且也是按照索引依次存放。 大部分抽屉都是关闭的,说明现在还没有人来访问。库房里有编号是421(PID(Process ID) 为 421 的进程)的一个进程正在查看文件;还有一个柜子已经打开,但旁边却没有人,可能是那个进程打开了文件,却由于某种原因没有正常关闭(比如异常或强行退出,甚至进程本身就忘了要关闭文件句柄),所以需要一条看门狗(右下角有一只小狗Watchdog)来进行处理,这代表对文件系统的监控。 4、总结、使用银行柜台业务总结linux系统处理任务流程: 这里像不像去银行柜台办理业务处理:1.请求输入—2.中断/异常处理——3.调度进程——4.服务进程处理——5.输出结果: 大家应该都去过银行办业务,由于银行的柜台就那么几个,办业务的人数又多,所以,去银行的第一件事就是排队取号: 银行柜台办理不同的业务类型: 普通业务(存取款)柜台(80web端口服务线程池):、例如有个有两个固定普通业务窗口(核心线程数为2)3位工作人员(最大线程数为3),等候座位(任务队列),一个规则《超出银行最大接待能力处理办法》(饱和等待策略)。 vip业务柜台(22 ssh端口直接优先处理): 办卡业务(办卡机直接办理):直接shell脚本处理。 如果大家办理普通业务, 首先是内存管理(叫号系统的号码): 由于银行的普通业务柜台就那么2个,办业务的人数又多。所以,去银行的第一件事就是排队取号,注意这个号号,并不代表去哪个柜台办业务,只是说你前面等待办业务的人数,不过需要等着叫号系统叫到你之后,才能办理业务,而且最终办理业务的柜台是随机的。好了,类比一下就是,银行柜台好比是物理内存,而排队序号就是虚拟地址,我们想办业务只能通过手里拿着的这个 “虚拟地址”,然后,必须通过叫号系统,将虚拟地址,即排队序号 翻译成具体的物理地址,即柜台号,我们才能办理业务。 进程管理: 进程属性: 每个柜台窗口都有窗口id(进程ID和服务端口号) 进程调度: 按照叫号系统的指示,办理普通业务的客户被安排到普通业务窗口,办理办卡业务的客户被安排办卡机办理。 线程调度: 例如有个有两个固定普通业务窗口(核心线程数为2)3位工作人员(最大线程数为3),等候座位(任务队列),一个规则《超出银行最大接待能力处理办法》(饱和拒绝策略)。 A客户(任务A)去银行(线程池)办理业务,但银行刚开始营业,窗口服务员还未就位(相当于线程池中初始线程数量为0),于是经理(线程池管理者)就安排1号工作人员(创建核心线程1去执行任务)接待A客户。 在A客户业务还没办完时,B客户(任务B)又来了,于是经理(线程池管理者)就安排2号工作人员(创建核心线程2去执行任务)接待B客户。 在A、B客户都没有办完业务的情况下,C客户(任务C)来了,于是经理(线程池管理者)就安排C客户先坐到等候座位上,并告知他:如果1、2号工作人员空出,C客户就可以前去办理业务。 此时D客户(任务D)又来了,(两个窗口都在忙,等候座位也满了)于是经理(线程池管理者)赶紧安排3号工作人员(创建非核心线程3去临时执行任务D)在大堂站着给D客户办理业务。 假如前面的业务都没有结束的时候E客户(任务E)又来了,此时2位窗口工作人员,和1位临时工作人员都在忙,等候座位也满了,于是经理只能按《超出银行最大接待能力处理办法》(饱和处理机制)拒接接待E客户。 如果是vip客户,可以直接被被安排到前面,抢占其他客户的时间办理,这显然是调度算法显然调高了vip客户(某个应用程序)的优先级,可以快速抵达“柜台”。 这当于Linux中进程的调度,它的头脑十分强大,需要按照各个应用程序的优先级顺序,必要时可以进行“抢占式调度”,抢占调度指的是一个高优先级进程是否可以强行夺取低优先级进程的处理器资源。如果可以强行夺取,就是可抢占的调度。这也是础光Linux实时性改造的一大亮点。 中断处理: 中断是指CPU接受到I/O设备发送的中断信号的一种响应。CPU会暂停正在执行的程序,保留CPU环境后自动转去执行该I/O设备的中断处理程序。执行完毕后回到断点。继续执行原来的程序。中断是由外部程序引起的所以称为外中断。小企鹅们会根据各类中断请求来进行CPU的工作安排。 类比银行业务,当前窗口柜台的客户X办理业务需要一些手续盖章而中断一段时间,但业务员可以继续办理其他客户业务,当客户X办理业务的手续盖章完成了,业务员继续唤醒客户X来办理。 三 . linux系统2: shell shell是系统的用户界面,提供了用户与内核进行交互操作的一种接口。它接收用户输入的命令并把它送入内核去执行,是一个命令解释器。另外,shell编程语言具有普通编程语言的很多特点,用这种编程语言编写的shell程序与其他应用程序具有同样的效果。 在Linux系统上,通常有好几种Linux shell可用。不同的shell有不同的特性,有些更利于创建脚本,有些则更利于管理进程。所有Linux发行版默认的shell都是bash shell。bash shell由GNU项目开发,被当作标准Unix shell。目前主要有下列版本的shell: 1.Bourne Shell:是贝尔实验室开发的。 2.BASH:是GNU的Bourne Again Shell,是GNU操作系统上默认的shell,大部分linux的发行套件使用的都是这种shell。 3.Korn Shell:是对Bourne SHell的发展,在大部分内容上与Bourne Shell兼容。 4.C Shell:是SUN公司Shell的BSD版本。 1、shell的类型 linux启动什么样的shell程序取决于用户ID配置。 在/etc/passwd文件中,在用户ID记录的第7个字段中列出了默认的shell程序。只要用户登录到某个虚拟控制台终端或是在GUI中启动终端仿真器,默认的shell程序就会开始运行。例如:用户root使用/bin/bash(bash shell)作为自己的默认shell程序. 不过还有另外一个默认shell是/bin/sh,它作为默认的系统shell,用于那些需要在启动时使用的系统shell脚本。你经常会看到某些发行版使用软链接将默认的系统shell设置成bash shell,如CentOS发行版: $ ls -l /bin/sh /bin/sh 相当于 /bin/bash --posix,使用 sh 调用执行脚本相当于打开了bash 的 POSIX 标准模式,它们之间的各种差异都是来自 POSIX 标准模式和bash的差异。 2、shell的父子关系 用于登录某个虚拟控制器终端或在GUI中运行终端仿真器时所启动的默认的交互shell,是一个父shell。 在CLI提示符后输入/bin/bash命令或其他等效的bash命令时,会创建一个新的shell程序。这个shell程序被称为子shell(child shell)。子shell也拥有CLI提示符,同样会等待命令输入。 例如:使用ps -f 使用ps -f的时候,显示出了两个进程: 第一个bash shell程序,也就是父shell进程,进程ID是3966191,运行的是bash shell程序。 第二个bash shell程序,即子shell进程,进程ID为3972365,对应的是命令ps -f。 在生成子shell进程时,只有部分父进程的环境被复制到子shell环境中。 3、常用的GUN命令总结 GUN是GNU的一种小工具,它是“GNU即不仅仅是UNIX”的缩写。GNU工具集是一系列用于UNIX操作系统的自由软件工具。这些工具提供了强大的功能,可以用于文件操作、文本处理、版本控制、编译和调试等任务。 下面是一些常见的GUN工具及其含义: 1). 文本处理工具 grep:用于在文件中搜索指定的字符串,并返回匹配的行。它支持正则表达式,可以进行高级搜索。 awk:一种强大的文本处理工具,它可以根据指定的规则对输入文件进行处理。它常用于提取、转换和格式化文本数据。 sed:流编辑器,用于对输入流进行文本转换。它可以在文件中查找和替换字符串,删除或插入行,并执行其他编辑操作。 2). 文件管理工具: GUN命令集还包含了用于文件管理的工具。 ls:命令用于列出目录内容, cp:命令用于复制文件, mv:命令用于移动或重命名文件, rm:命令用于删除文件等。 find:用于在指定目录中查找文件。它可以基于文件属性(如文件名、大小、时间戳等)进行搜索,并支持复杂的逻辑操作。 tar:用于创建和提取归档文件。它可以将多个文件和目录打包成一个单独的文件,也可以提取已打包的文件。 gzip:用于压缩文件。它使用Lempel-Ziv算法对文件进行压缩,以减小文件大小。 这些工具提供了对文件系统的基本操作。 3). 进程管理和系统监控: GUN命令集中的一些工具可用于管理和监控系统中运行的进程。 ps:命令用于列出当前运行的进程, top:命令用于实时监视系统资源的使用情况, kill:命令用于终止正在运行的进程等。 4). 网络工具 GUN命令集包含了一些网络工具,用于管理和配置网络连接。 ifconfig:命令用于配置网络接口, ping:命令用于测试网络连接的可达性, netstat:命令用于显示当前网络连接和端口状态等。 5). 软件包管理: GUN命令集中还包含了一些用于软件包管理的工具。 apt-get命令:用于在Debian和Ubuntu系统中安装和升级软件包, yum命令:用于在Red Hat和CentOS系统中进行相同的操作。 这些工具简化了软件安装和更新的过程。 6). 软件编译相关: make:用于自动构建软件项目。它根据指定的规则和依赖关系,自动编译和链接源代码文件,生成可执行文件或库文件。 gcc:GNU编译器集合,用于编译C、C++和其他支持的编程语言。它将源代码文件编译成可执行文件。 gdb:GNU调试器,用于调试程序。它可以在程序运行过程中暂停执行,并提供查看变量、堆栈和内存的功能。 四 . linux系统3: 文件管理系统 各操作系统使用的文件系统并不相同,例如,Windows98 以前的微软操作系统使用 FAT(FAT16)文件系统,Windows 2000 以后的版本使用 NTFS 文件系统,而 Linux 的正统文件系统是 Ext2。 在 CentOS 6.3 系统中,默认的文件系统是 Ext4,它是 Ext3(Ext2) 文件系统的升级版,在性能、伸缩性和可靠性方面进行了大量改进,变化可以说是翻天覆地的,比如: 向下兼容 Ext3; 最大 1EB 文件系统和 16TB 文件; 无限数量子目录; Extents 连续数据块概念; 多块分配、延迟分配、持久预分配; 快速 FSCK、日志校验、无日志模式、在线碎片整理、inode 增强、默认启用 barrier 等; Linux支持的常见文件系统 Linux 系统能够支持的文件系统非常多,除 Linux 默认文件系统 Ext2、Ext3 和 Ext4 之外,还能支持 fat16、fat32、NTFS(需要重新编译内核)等 Windows 文件系统。也就是说,Linux 可以通过挂载的方式使用 Windows 文件系统中的数据。Linux 所能够支持的文件系统在 "/usr/src/kemels/当前系统版本/fs" 目录中(需要在安装时选择),该目录中的每个子目录都是一个可以识别的文件系统。我们介绍较为常见的 Linux 支持的文件系统,如表 1 所示。 五 . linux系统4: 应用程序,用户态和内核态 应用程序是无法直接访问硬件资源的,需要通过通过内核SCI 层提供的接口来访问硬件资源。 Linux系统将自身划分为两部分,一部分为核心软件,即是kernel,也称作内核空间,另一部分为普通应用程序,这部分称为用户空间。 区分用户空间和内核空间的目的是为确保系统安全。在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。因为如果应用程序和内核在同一个保护级别,那么应用程序就有可能有意或者不小心进入了内核空间,破坏了内核空间的代码和数据,系统崩溃就不足为奇。所以CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3, 内核空间级别为“RING0”, 用户空间级别为RING3。 linux的内核是一个有机的整体。每一个用户进程运行时都好像有一份内核的拷贝,每当用户进程使用系统调用时,都自动地将运行模式从用户级转为内核级,此时进程在内核的地址空间中运行。 当应用程序进程执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(RING0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(RING3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。 内核态与用户态是操作系统的两种运行级别,跟intel cpu没有必然的联系, 如上所提到的intel cpu提供Ring0-Ring3四种级别的运行模式,Ring0级别最高,Ring3最低。Linux使用了Ring3级别运行用户态,Ring0作为 内核态,没有使用Ring1和Ring2。 内核空间和用户空间 x86 CPU采用了段页式地址映射模型。进程代码中的地址为逻辑地址,经过段页式地址映射后,才真正访问物理内存。 通常32位Linux内核地址空间划分0~3G为用户空间,3~4G为内核空间。64位内核地址空间划分是不同的。 32位与64位具体地址分布如下图: 64位地址时将0x0000,0000,0000,0000 – 0x0000,7fff,ffff,f000这128T地址用于用户空间。参见定义: #define TASK_SIZE_MAX ((1UL << 47) - PAGE_SIZE),注意这里还减去了一个页面的大小做为保护。 而0xffff,8000,0000,0000以上为系统空间地址。注意:该地址前4个都是f,这是因为目前实际上只用了64位地址中的48位(高16位是没有用的),而从地址0x0000,7fff,ffff,ffff到0xffff,8000,0000,0000中间是一个巨大的空洞,是为以后的扩展预留的。 而真正的系统空间的起始地址,是从0xffff,8800,0000,0000开始的,参见: #define __PAGE_OFFSET _AC(0xffff,8800,0000,0000, UL) 而32位地址时系统空间的起始地址为0xC000,0000。 另外0xffff,8800,0000,0000 – 0xffff,c7ff,ffff,ffff这64T直接和物理内存进行映射,0xffff,c900,0000,0000 – 0xffff,e8ff,ffff,ffff这32T用于vmalloc/ioremap的地址空间。 而32位地址空间时,当物理内存大于896M时(Linux2.4内核是896M,3.x内核是884M,是个经验值),由于地址空间的限制,内核只会将0~896M的地址进行映射,而896M以上的空间用做一些固定映射和vmalloc/ioremap。而64位地址时是将所有物理内存都进行映射。 内核态与用户态 用户态Ring3状态不能访问内核态Ring0的地址空间,包括代码和数据。(例如32位Linux进程的4GB地址空间,3G-4G部 分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护的数据)。用户运行一个程序,该程序所创建的进程开始是运行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过write,send等系统调用,这些系统调用会调用内核中的代码来完成操作,这时,必 须切换到Ring0,然后进入内核地址空间去执行这些代码完成操作,完成后,切换回Ring3,回到用户态。这样,用户态的程序就不能 随意操作内核地址空间,具有一定的安全保护作用。 处理器总处于以下状态中的一种: 1、内核态,运行于进程上下文,内核代表进程运行于内核空间; 2、内核态,运行于中断上下文,内核代表硬件运行于内核空间; 3、用户态,运行于用户空间。 从用户空间到内核空间有两种触发手段: 1.系统调用: 用户空间的应用程序,通过系统调用,进入内核空间。这个时候用户空间的进程要传递很多变量、参数的值给内核,内核态运行的时候也要保存用户进程的一些寄存器值、变量等。所谓的“进程上下文”,可以看作是用户进程传递给内核的这些参数以及内核要保存的那一整套的变量和寄存器值和当时的环境等。 2.中断: 硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。例如网卡发送一个数据包或硬盘驱动器提供一次 IO 请求等。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。所谓的“中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。
“我是吹牛,但我基本都实现了!”这句话出自华为余承东之口,其直爽的个性让人印象深刻,更突显了华为对自己技术的坚定信心。今日,余承东再次放出狠话:今年华为上市了别人想得到但做不出来的产品,明年会有大家想不到的产品,未来还有大家不敢想的产品!但有了华为三折叠屏手机的前例,他的这番话似乎并非吹牛。余承东表示,去年秋天,随着先锋计划,在历经连续5年制裁之后,华为重返了智能手机的赛道。今年是华为重返赛道的第一年,实现了快速地增长,也开启了华为终端未来十年的新征程。“我们一直说,我们要敢于做别人想得到但做不出来的产品,这款产品上市了,这就是全球首款商用三折叠屏手机,改写了手机行业的历史。还有我们的原生鸿蒙操作系统也正式发布,在迅速完善成熟商用中。史上最强大的Mate 70系列,盎然向新。”余承东还预告,明年还会有大家想不到的产品,未来还有大家不敢想的产品,敬请期待。据悉,12月28日到30日,华为将在深圳举办2024花粉年会。“特别感谢花粉朋友们长期以来的支持、信任与包容!我看到有花粉朋友报名了感谢大家的热情!期待你们的到来!”余承东说。华为Mate 70系列芯片实现100%国产日前,余承东在宝安中学进行了一场讲座,途中直接掏出了华为Mate 70手机,骄傲的称其实现了芯片的100%国产。尤其是SoC,Mate 70 Pro以上的机型这次还首发了全新一代的麒麟9020芯片。按照华为官方的数据,这让Mate 70系列相比上一代华为Mate 60 Pro+,操作流畅度提升39%,游戏帧率提升31%,整机性能提升40%。具体参数方面,麒麟9020的CPU部分包含1*泰山大核2.5GHz+3*泰山中核2.15GHz+4*小核1.6GHz,GPU为Maleoon 920。CPU的频率相较之前有所提升,同时GPU也是麒麟归来后首次换代,游戏性能相较麒麟9000s、麒麟9010明显增强。麒麟9020的安兔兔跑分在125万分左右,比麒麟9010高了30%。最重要的是,麒麟9020这次实现了CPU大中小核全自研,用上了全新自研的小核心,相比于公版小核心大幅增强。根据博主的拆解发现,麒麟9020芯片面积更大,同时还集成5G-A SOC及卫星通信模块。Mate 70系列还可以升级原生鸿蒙系统,是中国首个全栈自研的移动操作系统。自此,华为做到了血统最纯正的国产手机。对华芯片出口管制是“白费工夫”对中国的芯片限制出口管制策略,在美国商务部长看来,真的是失败了。如今距离卸任只剩几周,美国商务部长雷蒙多称,美国试图限制中国获得先进半导体技术的努力并没有阻碍中国的进步,在芯片竞赛中“试图阻挠中国是‘白费工夫’(a fool's errand)”。报道称,雷蒙多认为,努力将中国排除在“敏感技术”之外仍然很重要,但对华半导体出口管制只不过使中国向全球技术主导地位迈进的步伐“减速”而已。“打败中国的唯一方法就是走在他们前面。”她说,“我们必须跑得更快,在创新方面超越他们,这才是制胜之道。”事实上,中国半导体正在快速的崛起。最典型的一个例子就是,随着Mate 60的发布,华为带着新麒麟9000系列芯片重回智能手机领域,已经说明了美国封锁芯片的做法失败。
1什么是AEB自动紧急制动 自动紧急制动 Advanced/Automatic Emergency Braking; AEB AEB是一种汽车主动安全技术,能够实时检测车辆前方行驶环境,并在可能发生碰撞危险时自动启动车辆制动系统,使车辆减速,辅助驾驶员避免碰撞或减轻碰撞后果。 当系统计算出会有碰撞可能时,首先会通过声音、图标等警示驾驶员,若驾驶员没能对预警起到正确反应,再轻微震动制动踏板或方向盘来二次预警,过程中提前填充制动油路油压,以便全力制动能快速准确的完成。 简单点说AEB,就像你副驾驶的教练,能在危险的时候帮你踩刹车。 AEB常见变化: 自动紧急制动(AEB): 当车辆感知到即将发生碰撞时,车辆会独立停车,以避免发生碰撞,或者降低无法避免的严重程度。 向前自动紧急制动(AEB): 在汽车向前行驶时,会自动应用制动,以防止碰撞或减小冲击力。 后方自动紧急制动(AEB后部): 当汽车在倒车方向行驶时,会自动施加制动以防止碰撞或减小冲击力。 具有行人检测功能的自动紧急制动(AEB踏板): 在汽车前进时,会自动施加制动,以防止与行人或骑自行车的人发生碰撞或减少冲击力。 城市速度AEB(AEB-city): 在城市速度(通常为80公里或以下)下,会自动应用制动器以防止碰撞或减小冲击力。 高速公路速度AEB(高速公路AEB): 在高速公路速度(每小时80公里以上)时,会自动应用制动器以防止碰撞或减小冲击力。 2AEB是如何工作的 AEB系统和其他辅助系统一样,由感知、决策、执行三大部分组成,具体来说就是由雷达、摄像头作为传感器构成感知部分,传感器内置ECU或独立的外置ECU完成决策,并将制动请求通过总线发送至执行器,通常是ESP,也可以是其他装置,例如线控制动系统或独立的高压蓄能器控制器对车辆进行制动。 【感知】 常见的感知方案有三种。 视觉摄像头: 摄像头就像人眼一样,可跟踪识别行人障碍物等,感知距离大概在120米左右,但是不能精确计算与物体的相对距离,而且受不良天气的影响,因此单独采用摄像头方案的AEB系统非常少 障碍物识别,主流的主机厂一般使用Mobileye的成熟算法,测距主要是利用被识别障碍物在图像中的像素大小以及短时间差内的图像障碍物视差来实现。 毫米波雷达: 毫米波雷达的感知距离大概在150米以上,但是因为天线和尺寸的特性,雷达的角度分辨率也有限,但是较难识别行人等障碍物,而且雷达存在二次反射问题,容易出现误识别,因此单独采用雷达的AEB方案,也非常少。通过多普勒效应计算距离/差速。 _ 视觉摄像头 毫米波雷达 作用距离 100-120m 150-250m 测距精度 近/远 0.1m/1m 0.3m(远近一致) 光线与天气影响 显著 很小 物体高宽测量 精度高 精度低 车道线标识识别 有 无 行人识别准确度 高 低 成本 一般 一般 视觉摄像头与毫米波雷达系统对比 视觉摄像头融合雷达 为了能做出更可靠的AEB方案,大部分车厂将毫米波雷达与视觉摄像头结合起来,两者优势互补,视觉摄像头识别目标类型,用雷达较好的角度分辨率感知距离,判断与障碍物距离信息,然后相互确认,大幅度降低误判断 可靠性 目标真实可靠性高 互补性 全天候应用与远距离提前预警 高精度 大视角全距离条件下的高性能定位 识别能力 复杂对象的分类处理 成本 高性价比与选择灵活性 视觉与毫米波雷达融合方案优势 【决策】 决策就是用汽车的大脑(ECU)做判断,ECU能够根据传感器信息,然后按照设定的逻辑计算,得出执行命令,最后将执行命令发送给执行机构。 这里我们要提到一个词——碰撞时间TTC(Time-To-Collision)。 TTC是Time-To-Collision的缩写,直译为碰撞时间。海沃德(1972)将TTC定义为:“如果两个车辆以现在的速度和相同的路径继续碰撞,则需要碰撞的时间”。在交通冲突技术的研究中,TTC已被证明是衡量交通冲突严重程度和区分关键行为与正常行为的有效手段。一些研究的结果指出直接使用TTC作为交通决策的线索。车辆之间未来相互作用的预测涉及为受试车辆以及所有可能发生相互作用的车辆创建预测轨迹,以查看是否可能发生碰撞。 在TTC算法中,车辆被视为二维平面。每一辆都由位于平面中特定位置的矩形表示。每辆车都有速度和加速度,速度与加速度都是矢量。 每辆“主体”车辆与附近的车辆会发生相互作用,不存在先导车辆或跟随车辆。主体车辆的动作遵循三条规则: 1. 跟随前方的车辆 2. 避免碰撞 3. 基于TTC的数值来调整所采取的动作的强度 TTC是针对每两辆相互足够接近车辆来计算相互时间长。根据其老位置、新速度矢量和新加速度矢量计算车辆的新坐标。它的新速度矢量同样是从它的旧速度和新加速度矢量计算出来的。通过对期望轨迹、道路几何形状、交通控制(例如,停止标志、交通信号和速度限制)以及邻近车辆的接近来确定加速度矢量。如果不引起任何碰撞,加速度被认为是可接受的。 在车辆行驶时,实时地计算出本车与前车在当前运动状态下,继续运动直到发生碰撞所需要的时间(即TTC),来与事先设定好的阀值进行比较:当 TTC 值小于 FCW 阀值时,系统采用视觉、听觉或触觉向驾驶员报警;当 TTC 小于 AEB 阀值时,系统以一定的减速度采取紧急制动。 【执行】 执行可以简单的理解为驾驶员帮你踩刹车。 执行机构,通常是通过ESP或其它装置,例如i—Booster或者独立的高压蓄能器控制器——对车辆刹车系统进行控制制动。 但是在执行刹车之前,一般都会有碰撞预警系统做提示,让你自己处理危险,或者有个心理准备。 提醒阶段主要是通过声学和光学的方式提醒驾驶者对车辆即将可能发生碰撞进行接管,并对制动系统进行提前减压。同时还会根据车辆实际的配置对一些功能进行调节,比如可变悬架。 到了预制动阶段,AEB系统首先会试图通过短促的制动来唤醒驾驶员,同时车辆也会对安全带进行预紧。此时制动系统开始对刹车盘施加制动力,但通常只有全部制动能力的30%。此阶段仍然可以通过驾驶员的干涉来完全避免碰撞。 而部分制动阶段时AEB系统开始使用50%的制动力来为车辆减速,同时配备自动车窗和天窗的车辆会开始主动关闭,避免驾驶员在接下来可能发生的碰撞中被抛出窗外,在进入部分制动时,AEB系统也会打开双闪警示灯提醒后车。此时如果驾驶员进行干预,仍然有可能避免发生碰撞。 最后是全力制动阶段,在这一阶段AEB系统将会放弃依靠驾驶员的制动行为,并通过执行器进行100%刹车力度的制动。与此同时车辆也会收到信号开始着手为接下来可能存在的碰撞风险做好准备,比如将安全带收紧等。 整个执行过程的持续时间通常只有两三秒钟,我们甚至很难通过身体的感受来区分第二和第三阶段的区别。通常来说AEB系统会根据危险等级依次进入四个阶段,但也有一些情况会跳过其中某个或某几个阶段。比如面对突然出现的行人,或是前方障碍物与当前车辆的距离迅速缩短。 3AEB工作范围 可以工作在不同的道路上。 目前大部分车厂采用视觉融合毫米波雷达的方案,具有车辆识别和行人识别的功能。 车辆识别: 在40km/h以内,可以做到避免与静止车辆碰撞 在与前方运动的速度差小于40km/h以内,可以做到避免碰撞 如果运动时速与前车大于40km/h时,降低事故损伤程度 行人识别:(只能探测到身高80cm以上的人) 时速30km/h以内,避免与行人发生碰撞 时速在30km/h-90km/h之间,有可能会撞上,但是可以降低损伤事故 时速超过90km/h,行人识别功能关闭 车速过低<5km/h时,AEB不工作,速度太高>150km/h时,也不工作,目前的AEB并不是任何车速下都能刹车 除了速度范围,一般车型的AEB也只能识别车辆和行人两种,虽然部分AEB带有骑行识别功能,但识别率很低 4AEB的技术重点是什么 传感器融合方案 传感器数据融合的基本原理主要是综合多个传感器获取的数据和信息,把多传感器在空间和时间上冗余或互补信息依据某种准则来进行组合,获得对被测对象的一致性描述 传感器融合的优势: 1.增强系统生存能力 2.扩展空间覆盖范围 3.扩展时间覆盖范围 4.提高可信度 5.降低信息的模糊度 6.改进探测性能 7.提高空间分辨率 8.增加了测量空间维数 9.成本低、质量轻、占空小 首先摄像头和毫米波雷达分别针对观测目标收集数据,然后对各传感器的输出数据进行特征提取与模式识别处理,并将目标按类别进行准确关联,最后利用融合算法将同一目标的所有传感器数据进行整合,从而得出关于目标威胁性的一致性结论。 数据融合也有不同的策略,比如有的方案会选择将不同传感器各自处理生成的目标数据进行融合,有些会选择将不同传感器的原始数据进行融合,避免一些原始数据的丢失。在智能驾驶场景下,传感器的数据融合大致有3种策略:数据级、特征级和决策级。 数据级融合 数据级融合是最低层次的融合,直接对传感器的感测数据进行融合处理,然后基于融合后的结果进行特征提取和判断决策。这种融合处理方法的主要优点是:只有较少数据量的损失,并能提供其他融合层次所不能提供的其他细微信息,所以精度最高。他的局限性包括: 1.所要处理的传感器数据量大,故障处理代价高,处理时间长,实时性差 2.这种融合是在信息的最底层进行的,传感器信息的不确定性、不完全性和不稳定性要求在融合时有较高的纠错处理能力 3.它要求传感器是同类的,即提供对同一观测对象的同类观测数据 4.数据通信量大,抗干扰能力差 此级别的数据融合用于多源图像复合、图像分析和理解以及同类雷达波形直接合成 特征级融合 特征级融合属于中间层次的融合,先由每个传感器抽象出自己的特征向量(可以是目标的边缘、方向和速度等信息),融合中心完成的是特征向量的融合处理。一般来说,提取的特征信息应是数据信息的充分表示量或充分统计量。其优点在于实现了可观的数据压缩,降低对通信的要求,有利于实时处理,但由于损失了一部分有用信息,使得融合性能有所下降。 特征级融合可以划分目标状态信息融合和目标特征信息融合两大类。 目标状态信息融合主要应用于目标跟踪,融合处理首先对传感器进行数据处理,完成数据校准,然后进行数据相关的状态估计。具体数学方法包括卡尔曼滤波理论、联合概率数据关联、多假设法、交互式多模型法和序贯处理理论。 目标特征信息融合实际属于模式识别问题,常见的数学方法有参量模板法、特征压缩和聚类方法、人工神经网络、K阶最近邻法等。 决策级融合 决策及融合是一种高层次的融合,先由每个传感器基于自己的数据做出决策,然后融合中心完成的是局部决策的融合处理。决策融合是三级融合的最终结果,是直接针对具体决策目标的,融合结果直接影响决策水平。这种处理方法数据损失量大,相对来说精度最低,但通信量小,抗干扰能力强,对传感器依赖小,不要求是同质传感器,融合中心处理代价低。常见的算法由Bayes推断、专家系统、D-S证据推理、模糊集理论等。 特征级和决策级的融合不要求多传感器是同类的。另外由于不同融合级别的融合算法各有利弊,所以为了提高信息融合技术的速度和精准,需要开发高效的局部传感器处理策略以及优化融合中心的融合规则。 【信息融合的主要技术和方法】 ・信号处理与估计理论方法 信号处理与估计理论方法包括用于图像增强与处理的小波变换技术、加权平衡、最小二乘、Kalman滤波等线性估计技术,以及扩展Kalman滤波(EKF),Gauss和滤波(GPS)等非线性估计技术等 ・统计推断方法 统计推断方法包括经典推理、Bayes理论、证据推理、随机集理论以及支持向量机理论等 ・信息论方法 信息论方法运用优化信息度量的手段融合多源数据,从而获得问题的有效解决。经典算法有熵方法、最小描述长度方法(MDL)等。 ・人工智能方法 人工智能方法包括模糊逻辑、神经网络、遗传算法、基于规则的推理以及专家系统、逻辑模板法、品质因数法(FOM)等。 5AEB的品牌名称 阿尔法罗密欧:Autonomous Emergency Braking 奥迪:Automatic Brake Assist 宝马:Driving Assistant Plus 福特:Active City Stop 霍尔顿:Automatic Emergency Braking City Stop 本田:Collision Mitigation Braking System 现代:Autonomous Emergency Braking 起亚:Autonomous Emergency Braking 路虎:Autonomous Emergency Braking 雷克萨斯:Pre-collision Safety System with Brake Assist 马自达:Smart City Brake Support 梅赛德斯·奔驰:PRE-SAFE Brake 迷你版:City Collision Mitigation 三菱:Forward Collision Mitigation 日产:Intelligent Emergency Braking with Forward-Collision Warning 标致:Active City Brake 斯柯达:Multi-Collision Braking 斯巴鲁:Pre-Collision Braking System (Eyesight) 丰田:Pre-Crash Safety System 大众:City Emergency Braking 沃尔沃:City Safety 6AEB使用注意事项 关于AEB大家有一个普遍的误解——好的AEB系统应该避免一切碰撞。 在目前的技术条件下,工程师并不能完全排除AEB系统发生各种故障或错误,却依然能发出减速度请求的情况,例如由于识别到错误的目标而导致AEB触发,车辆突然减速。特别地,在高速公路上行驶的时,如果过度减速,很容易导致追尾事故。出于功能安全(Functional Safety)的考虑,目前市场上的多数AEB系统最多允许车速降低60kph。当车辆减速达到这一限制的时候,制动干预应该逐渐退出(仅制动退出,并非AEB系统退出),由驾驶者接管。
引言:可使用频谱的增加会推动无线设备逐步改进,但增加这些额外的频谱有时候会影响到某些区域,导致其中的频段相互重叠,此外,由于RF路径增加,会导致系统发热量随之增加,而发热反过来又会影响滤波器的性能。
引言:分立式开关也叫射频传导开关,作用是将多路射频信号中的任一路或几路通过控制逻辑连通,以实现不同信号路径的切换,包括接收与发射的切换、不同频段间的切换等。
电路问题计算的先决条件是正确识别电路,搞清楚各部分之间的连接关系。对较复杂的电路应先将原电路简化为等效电路,以便分析和计算。 识别电路的方法很多,现结合具体实例介绍十种方法。 01 特征识别法 串并联电路的特征是:串联电路中电流不分叉,各点电势逐次降低,并联电路中电流分叉,各支路两端分别是等电势,两端之间等电压。根据串并联电路的特征识别电路是简化电路的一种最基本的方法。 举例:试画出图 1 所示的等效电路。 解:设电流由 A 端流入,在 a 点分叉,b 点汇合,由 B 端流出。支路 a—R1—b 和 a—R2—R3(R4)—b 各点电势逐次降低,两条支路的 a、b 两点之间电压相等,故知 R3 和 R4 并联后与 R2 串联,再与 R1 并联,等效电路如图 2 所示。 02 伸缩翻转法 在实验室接电路时常常可以这样操作,无阻导线可以延长或缩短,也可以翻过来转过去,或将一支路翻到别处,翻转时支路的两端保持不动;导线也可以从其所在节点上沿其它导线滑动,但不能越过元件。这样就提供了简化电路的一种方法,我们把这种方法称为伸缩翻转法。 举例:画出图 3 的等效电路。 解:先将连接 a、c 节点的导线缩短,并把连接 b、d 节点的导线伸长翻转到 R3—C—R4 支路外边去,如图 4。 再把连接 a、c节点的导线缩成一点,把连接 b、d 节点的导线也缩成一点,并把 R5 连到节点 d 的导线伸长线上(图 5)。由此可看出 R2、R3 与 R4 并联,再与 R1 和 R5 串联,接到电源上。 03 电流走向法 电流是分析电路的核心。从电源正极出发(无源电路可假设电流由一端流入另一端流出)顺着电流的走向,经各电阻绕外电路巡行一周至电源的负极,凡是电流无分叉地依次流过的电阻均为串联,凡是电流有分叉地分别流过的电阻均为并联。 举例:试画出图 6 所示的等效电路。 解:电流从电源正极流出过 A 点分为三路(AB 导线可缩为一点),经外电路巡行一周,由 D 点流入电源负极。第一路经 R1 直达 D 点,第二路经 R2 到达 C 点,第三路经 R3 也到达 C 点,显然 R2 和 R3 接联在 AC 两点之间为并联。二、三路电流同汇于 c 点经 R4 到达 D 点,可知 R2、R3 并联后与 R4 串联,再与 R1 并联,如图 7 所示。 04 等电势法 在较复杂的电路中往往能找到电势相等的点,把所有电势相等的点归结为一点,或画在一条线段上。当两等势点之间有非电源元件时,可将之去掉不考虑;当某条支路既无电源又无电流时,可取消这一支路。我们将这种简比电路的方法称为等电势法。 举例:如图 8 所示,已知 R1 = R2 = R3 = R4 = 2Ω ,求 A、B 两点间的总电阻。 解:设想把 A、B 两点分别接到电源的正负极上进行分析,A、D 两点电势相等,B、C 两点电势也相等,分别画成两条线段。电阻 R1 接在 A、C 两点,也即接在 A、B 两点;R2 接在 C、D 两点,也即接在 B、A 两点;R3 接在 D、B 两点,也即接在 A、B 两点,R4 也接在 A、B 两点,可见四个电阻都接在 A、B 两点之间均为并联(图 9)。所以,PAB=3Ω。 05 支路节点法 节点就是电路中几条支路的汇合点。所谓支路节点法就是将各节点编号(约定:电源正极为第 1 节点,从电源正极到负极,按先后次序经过的节点分别为 1、2、3……),从第 1 节点开始的支路,向电源负极画。可能有多条支路(规定:不同支路不能重复通过同一电阻)能达到电源负极,画的原则是先画节点数少的支路,再画节点数多的支路。然后照此原则,画出第 2 节点开始的支路。余次类推,最后将剩余的电阻按其两端的位置补画出来。 举例:画出图 10 所示的等效电路。 解:图 10 中有 1、2、3、4、5 五个节点,按照支路节点法原则,从电源正极(第 1 节点)出来,节点数少的支路有两条:R1、R2、R5 支路和 R1、R5、R4 支路。取其中一条 R1、R2、R5 支路,画出如图 11。 再由第 2 节点开始,有两条支路可达负极,一条是 R5、R4,节点数是 3,另一条是 R5、R3、R5,节点数是 4,且已有 R6 重复不可取。所以应再画出 R5、R4 支路,最后把剩余电阻 R3 画出,如图 12 所示。 06 几何变形法 几何变形法就是根据电路中的导线可以任意伸长、缩短、旋转或平移等特点,将给定的电路进行几何变形,进一步确定电路元件的连接关系,画出等效电路图。 举例:画出图 13 的等效电路。 解:使 ac 支路的导线缩短,电路进行几何变形可得图 14,再使 ac 缩为一点,bd 也缩为一点,明显地看出 R1、R2 和 R5 三者为并联,再与 R4 串联(图 15)。 07 撤去电阻法 根据串并联电路特点知,在串联电路中,撤去任何一个电阻,其它电阻无电流通过,则这些电阻是串联连接;在并联电路中,撤去任何一个电阻,其它电阻仍有电流通过,则这些电阻是并联连接。 举例:仍以图 13 为例,设电流由 A 端流入,B 端流出,先撤去 R2,由图 16 可知 R1、R3 有电流通过。再撤去电阻 R1,由图 17 可知 R2、R3 仍有电流通过。同理撤去电阻 R3 时,R1、R2 也有电流通过由并联电路的特点可知,R1、R2 和 R3 并联,再与 R4 串联。 08 独立支路法 让电流从电源正极流出,在不重复经过同一元件的原则下,看其中有几条路流回电源的负极,则有几条独立支路。未包含在独立支路内的剩余电阻按其两端的位置补上。应用这种方法时,选取独立支路要将导线包含进去。 举例:画出图 18 的等效电路。 方案一:选取 A—R2—R3—C—B 为一条独立支路,A—R1—R5—B 为另一条独立支路,剩余电阻 R4 接在 D、C 之间,如图 19 所示。 方案二:选取 A—R1—D—R4—C—B 为一条独立支路,再分别安排 R2、R3 和 R5,的位置,构成等效电路图 20。 方案三:选取 A—R2—R3—C—R4—D—R5—B 为一条独立支路,再把 R1 接到 AD 之间,导线接在 C、B 之间,如图 21 所示,结果仍无法直观判断电阻的串并联关系,所以选取独立支路时一定要将无阻导线包含进去。 09 节点跨接法 将已知电路中各节点编号,按电势由高到低的顺序依次用 1、2、3……数码标出来(接于电源正极的节点电势最高,接于电源负极的节点电势最低,等电势的节点用同一数码,并合并为一点)。然后按电势的高低将各节点重新排布,再将各元件跨接到相对应的两节点之间,即可画出等效电路。 举例:画出图 22 所示的等效电路。 解:节点编号如图 22 中所示。节点排列,将 1、23 节点依次间隔地排列在一条直线上,如图 23。元件归位,对照图 22,将 R1、R2、R3、R4 分别跨接在排列好的 1、2 的等效电路如图 24。 010 电表摘补法 若复杂的电路接有电表,在不计电流表 A 和电压表 V 的内阻影响时,由于电流表内阻为零,可摘去用一根无阻导线代替;由于电压表内阻极大,可摘去视为开路。用上述方法画出等效电 搞清连接关系后,再把电表补到电路对应的位置上。 举例:如图 25 的电路中,电表内阻的影响忽略不计,试画出它的等效电路。 解:先将电流去,用一根导线代摘替,再摘去电压表视为开路,得图 26。然后根据图 25 把电流表和电压表补接到电路中的对应位置上,如图 27 所示。
定时器的使用场景主要有两种。 (1)周期性任务 这是定时器最常用的一种场景,比如 tcp 中的 keepalive 定时器,起到 tcp 连接的两端保活的作用,周期性发送数据包,如果对端回复报文,说明对端还活着;如果对端不回复数据包,就会判定对端已经不存在了;再比如分布式系统中,各个组件之间的心跳报文也是定时发送来维护组件之间的状态。 (2)兜底功能 一些不立即执行的任务的时间底线。比如 tcp 中的延迟 ack 功能,说的就是在接收到一个报文的时候,并不会立即向对方回复 ack,而是会看看本端最近是不是会发送报文,如果是的话,那么 ack 就跟随这个报文一块发送, 这样可以减少链路上的报文数量,提高带宽利用率。如果本端很长时间内没有数据发向对端呢,当前这个线程不会一直在这里等待,而是使用一个定时器来完成后边的工作,也就是说最多可以等待多长时间,即等待的底线,如果超过这个底线之后还没有等到发送数据,那么这个定时器就会直接将 ack 发送出去。重传定时器,0 窗口探测定时器,也起到了兜底的作用。定时器通过异步的方式解放了线程,有了定时器就不需要线程在这里等待。 tcp 中使用的定时器有多个,本文主要有介绍以下 6 个。6 个定时器可以按照 tcp 连接的生命周期进行划分,划分结果如下表所示: 定时器分类 定时器 定时器成员 所在结构体 超时处理函数 建立连接过程 syn + ack 定时器 rsk_timer struct request_sock reqsk_timer_handler() 数据传输过程 重传定时器 icsk_retransmit_timer struct inet_connection_sock tcp_retransmit_timer() 数据传输过程 延时 ack 定时器 icsk_delack_timer struct inet_connection_sock tcp_delack_timer() 数据传输过程 保活定时器 sk_timer struct sock tcp_keepalive_timer() 数据传输过程 窗口探测定时器 icsk_retransmit_timer struct inet_connection_sock tcp_probe_timer() 断开连接过程 TIME_WAIT 定时器 tw_timer struct inet_timewait_sock tw_timer_handler() 不同的定时器,维护的 socket 是不一样的。syn + ack 定时器在 struct request_sock 中维护,TIME_WAIT 定时器在 struct inet_timewait_sock 中维护,这两个定时器也是只在建立连接阶段或断开连接阶段存在,并且前者是服务端需要使用的定时器,后者是主动断开连接的一方需要使用的定时器,并不是连接的每一端都需要。数据传输过程中使用的定时器是连接的两端都要使用到的定时器。 1 连接建立过程定时器 1.1 syn 定时器 在介绍 syn + ack 定时器之前,先介绍一下 syn 定时器。顾名思义,syn 定时器就是重传 syn 包的定时器。之所以上边表格中没有单独列出来 syn 定时器,是因为 syn 定时器就是重传定时器。 syn 定时器即发起连接的一方(客户端),发送 syn 包之后,会启动一个定时器,这个定时器和后边讲的连接建立完成之后的重传定时器是同一个定时器。作用也是一样的,即发送 syn 包之后,如果在超时时间之内没有收到 syn + ack 报文,便会重传 syn 包。 发送 syn 包和启动定时器的工作在 tcp_connect() 函数中完成。这个定时器只有客户端才需要使用,所以不是在 socket 的初始化函数中创建的,而是在 tcp_connect() 函数中创建的。 syn 包最大重传次数可通过 /proc/sys/net/ipv4/tcp_syn_retries 配置,默认是 6。 int tcp_connect(struct sock *sk){ struct sk_buff *buff; // 构造一个 syn 报文 tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN); // 将报文放入重传队列中,重传队列使用红黑树来维护 tcp_rbtree_insert(&sk->tcp_rtx_queue, buff); // 发送 syn 包 err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) : tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); // 启动重传定时器 inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX); return 0;} 1.2 syn + ack 定时器 服务端收到 syn 之后,便会进行第二次握手,即发送 syn + ack 报文。syn + ack 定时器和 syn 定时器的作用类似,也是检测发送 syn + ack 报文之后,在一定时间内有没有收到第三次握手的 ack 报文,如果没有收到,该定时器超时之后便会重传 syn + ack。 syn + ack 定时器的创建调用栈是 : tcp_conn_request() 调用 inet_csk_reqsk_queue_hash_add() 调用 reqsk_queue_hash_req() 当服务端收到 syn 报文时,说明有新的连接请求,该请求在函数 tcp_conn_request() 中处理,在该函数中的主要工作有三个: ① 申请一个 struct request_sock,然后将之加入到 ehash 中,便于第三次握手到来之后查找到这个套接字 ② 向对端发送 syn + ack 报文,即第二次握手 ③ 启动 syn + ack 定时器 syn + ack 报文,同样也有最大重传次数限制,可以通过配置 /proc/sys/net/ipv4/tcp_synack_retries 进行修改,默认是 5。 2 数据传输过程中的定时器 ESTABLISHED 状态下的定时器包括重传定时器,延迟 ack 定时器,窗口探测定时器(又叫坚持定时器)以及保活定时器。这四个定时器在函数 tcp_init_xmit_timers() 创建,该函数被 tcp_init_sock() 调用,也就是说不管是客户端还是服务端都会创建这四个定时器。 void tcp_init_xmit_timers(struct sock *sk){ // 创建三个定时器,分别是重传定时器,延时 ack 定时器,保活定时器 // 三个定时器的超时处理函数即后三个入参 inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer); ...} 2.1 重传定时器 重传定时器,简单来说就是发送侧发送一个报文之后,就启动一个定时器等接收方的 ack,如果超时没有等到 ack,那么发送方就会认为发生了丢包,然后会重新发送这个报文;反之,如果在超时时间内收到了对端回应的 ack, 说明接收侧已经收到了这个报文,发送侧就可以放心地把这个报文从重传队列中取出,然后释放报文占用的资源了。 重传定时器示意图如下,发送方发送报文序列号 1000,长度为 200,发送之后便会启动重传定时器。正常情况下,在定时器超时之前,接收方会返回 ack,如果定时器超时的时候没有收到 ack,发送方便会认为这个报文丢失,从而会重传这个报文。 (1)什么时候启动重传定时器 ? 发包路径 // 函数 tcp_write_xmit() 中会调用 tcp_transmit_skb() 进行发包// 如果 tcp_transmit_skb() 返回成功,则调用函数 tcp_event_new_data_sent()// 在函数 tcp_event_new_data_sent() 中将报文放入重传队列中,同时启动重传定时器static void tcp_event_new_data_sent(struct sock *sk, struct sk_buff *skb){ // packets_out 表示发送出去,但是还没有收到 ack 的报文 // 在该函数的后边会更新这个变量,把刚发送的报文加上去 // 当收到 ack 报文的时候会对这个变量做减法 unsigned int prior_packets = tp->packets_out; // 更新 snd_nxt WRITE_ONCE(tp->snd_nxt, TCP_SKB_CB(skb)->end_seq); // 将 skb 从发送队列中移除,然后将 skb 放入重传队列 // 报文发向 ip 层成功之后并不能立即释放 skb, 因为报文在链路上可能会丢失 // 所以先将报文移入重传队列,如果这个报文在链路上丢了的话还可重传 // 只有收到这个报文的 ack 时,说明接收侧已经收到了这个报文 // 这个时候才可以将报文从重传队列中移除,释放 skb 资源 __skb_unlink(skb, &sk->sk_write_queue); tcp_rbtree_insert(&sk->tcp_rtx_queue, skb); // 更新 packets_out tp->packets_out += tcp_skb_pcount(skb); // prior_packets 即不包括这次发送的报文,之前发送出去但是还没有确认的报文 // 如果都已经确认了,说明重传定时器这个时候没有工作,需要启动重传定时器 // 如果还有没被确认的,说明上次发包的时候就已经启动了重传定时器,并且没有超时 // 这种情况下就不需要再次启动重传定时器了 // 具体启动重传定时器的工作在 tcp_rearm_rto() 中完成 if (!prior_packets || icsk->icsk_pending == ICSK_TIME_LOSS_PROBE) tcp_rearm_rto(sk);} // 函数 tcp_rearm_rto() 中首先计算 rto,即重传定时器的超时时间// 由此可见重传定时器的超时时间不是固定不变的,而是和链路状态有关系// 计算 rto 之后便会通过函数 tcp_reset_xmit_timer() 启动重传定时器void tcp_rearm_rto(struct sock *sk){ // 如果 packets_out 是 0,说明发送出去的报文已经全部确认,则可以停掉重传定时器 if (!tp->packets_out) { inet_csk_clear_xmit_timer(sk, ICSK_TIME_RETRANS); } else { u32 rto = inet_csk(sk)->icsk_rto; /* Offset the time elapsed after installing regular RTO */ if (icsk->icsk_pending == ICSK_TIME_REO_TIMEOUT || icsk->icsk_pending == ICSK_TIME_LOSS_PROBE) { s64 delta_us = tcp_rto_delta_us(sk); /* delta_us may not be positive if the socket is locked * when the retrans timer fires and is rescheduled. */ rto = usecs_to_jiffies(max_t(int, delta_us, 1)); } tcp_reset_xmit_timer(sk, ICSK_TIME_RETRANS, rto, TCP_RTO_MAX); }} (2) 收到 ack 报文的时候如何改变重传定时器 收到 ack 报文之后,如果发现发送的报文都已经被确认,那么就会停掉重传定时器;否则,则会重启重传定时器。 // tcp_ack() 函数处理接收到的 ack 报文// tcp_ack() 函数中调用 tcp_clean_rtx_queue() 来将已经 ack 的报文从重传队列中移除,// 同时对 tp->packets_out 做减法// tcp_clean_rtx_queue() 中会判断是不是有新的报文被确认,// 如果是,则返回的 flag 中包含 FLAG_SET_XMIT_TIMER 标志// 在 tcp_ack() 中就会重置重传定时器static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag){ // 如果有新的数据被确认,则返回的 flag 中带有标志 FLAG_SET_XMIT_TIMER flag |= tcp_clean_rtx_queue(sk, skb, prior_fack, prior_snd_una, &sack_state, flag & FLAG_ECE); // FLAG_SET_XMIT_TIMER 这个标志说明有数据被确认, // 这种情况下就需要重新设置重传定时器 // tcp_set_xmit_timer() 最终会调用到 tcp_rearm_rto() // 在 tcp_rearm_rto() 中判断, // 如果发送出去的报文都已经确认,则停止重传定时器,否则 reset 重传定时器 if (flag & FLAG_SET_XMIT_TIMER) tcp_set_xmit_timer(sk);} (3) 重传定时器回调函数中如何重传 ? 重传定时器超时,最终会调用函数 tcp_retransmit_timer() 进行重传。在该函数中主要做的工作有三个: ① 从重传队列中取出第一个报文,进行重传。 ② 重传之前要判断,重传次数是不是已经达到最大值,如果达到最大值,则放弃重传,设置套接字为错误状态。重传次数并不是无限的,而是有最大值限制。放弃重传的判断条件有两个,分别是时间维度和数量维度,函数 tcp_write_timeout() 中进行具体判断。 ③ 发生重传说明存在丢包,这种情况下进入 loss 状态。 void tcp_retransmit_timer(struct sock *sk){ struct tcp_sock *tp = tcp_sk(sk); struct net *net = sock_net(sk); struct inet_connection_sock *icsk = inet_csk(sk); struct request_sock *req; struct sk_buff *skb; // tp->packets_out 为 0,说明发送的报文都已经 ack 了 // 没有报文需要重传,直接 return if (!tp->packets_out) return; // 从重传队列中取出第一个报文 skb = tcp_rtx_queue_head(sk); if (WARN_ON_ONCE(!skb)) return; // 判断重传是否超时,如果超时,则将套接字设置为错误状态,然后退出 // 将套接字设置为错误状态通过函数 tcp_write_err() 完成 // 重传采用退避策略,重传定时器超时时间倍数增长 // 最小重传时间是 0.5s,最大是 120s,由下边两个宏来定义 // #define TCP_RTO_MAX ((unsigned)(120*HZ)) // #define TCP_RTO_MIN ((unsigned)(HZ/5)) if (tcp_write_timeout(sk)) goto out; // 进入 loss 状态 tcp_enter_loss(sk); // 重传报文 icsk->icsk_retransmits++; if (tcp_retransmit_skb(sk, tcp_rtx_queue_head(sk), 1) > 0) { /* Retransmission failed because of local congestion, * Let senders fight for local resources conservatively. */ inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, TCP_RESOURCE_PROBE_INTERVAL, TCP_RTO_MAX); goto out; } out_reset_timer: // 计算下次重传超时时间并重置重传定时器 if (sk->sk_state == TCP_ESTABLISHED && (tp->thin_lto || net->ipv4.sysctl_tcp_thin_linear_timeouts) && tcp_stream_is_thin(tp) && icsk->icsk_retransmits <= TCP_THIN_LINEAR_RETRIES) { icsk->icsk_backoff = 0; icsk->icsk_rto = min(__tcp_set_rto(tp), TCP_RTO_MAX); } else { /* Use normal (exponential) backoff */ icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX); } inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, tcp_clamp_rto_to_user_timeout(sk), TCP_RTO_MAX);out:;} 需要C/C++ Linux服务器架构师学习资料加qun579733396获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享 2.2 延时 ack 定时器 当接收到数据之后,并不一定是立即发送 ack。而是等待一段时间,如果在这段时间之内,有发往对方的数据,则 ack 随着该数据一块发送;如果在超时时间之内,没有数据发往对方,则在定时器回调函数中单独发送 ack。 延时 ack 也叫捎带 ack,相比于收到一个报文之后就立即发送 ack,延时 ack 可以减少链路上纯 ack 报文的比例,提高网络带宽利用率。 接收侧收到数据之后会调用函数 __tcp_ack_snd_check(),在这个函数中判断是不是需要立即发送 ack,如果需要立即发送 ack,则立即发送;否则的话,如果满足发送延时 ack 的条件,则调用函数 tcp_send_delayed_ack() 进行发送延时 ack 的逻辑。延时 ack 定时器的最小超时时间是 40ms, 最大超时时间是 200ms,分别用宏 TCP_DELACK_MIN 和 TCP_DELACK_MAX 来定义。 static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible){ struct tcp_sock *tp = tcp_sk(sk); unsigned long rtt, delay; // 收到的报文大于 mss // 或者设置了 quick ack // 或者设置了 ICSK_ACK_NOW // 直接发送 ack if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss && (tp->rcv_nxt - tp->copied_seq < sk->sk_rcvlowat || __tcp_select_window(sk) >= tp->rcv_wnd)) || tcp_in_quickack_mode(sk) || /* Protocol state mandates a one-time immediate ACK */ inet_csk(sk)->icsk_ack.pending & ICSK_ACK_NOW) { send_now: tcp_send_ack(sk); return; } // 延时 ack 的逻辑在 tcp_send_delayed_ack() 中进行处理 if (!ofo_possible || RB_EMPTY_ROOT(&tp->out_of_order_queue)) { tcp_send_delayed_ack(sk); return; } ...} 如果想要接收到报文之后就立即发送 ack,那么需要设置 socket 选项 TCP_QUICKACK。socket 选项中,有一些设置之后就会一直生效,比如 SO_RCVTIMEO 选项,可以设置接收数据的超时时间,如果阻塞这么长时间,数据还没有到来,那么 recv() 就会返回。还有一些选项,设置之后并不是一直生效,比如 TCP_QUICKACK,设置之后,就会立即回应 ack,但是这个选项并不一定一直生效,还会受到 tcp 协议栈内部判断的影响,所以需要每次收到数据之后都重新设置一次这个选项。 2.3 窗口探测定时器 在建立 tcp 连接时,两端会向对方通告自己的接收窗口大小。接收窗口用于流量控制,tcp 发送数据时不能超过对端接收窗口的大小。 如果出现发送方的发送速度大于接收方的接收速度,或者接收侧应用长时间没有从接收缓冲区接收数据的时候,接收窗口会变成 0,并将 0 窗口通知给发送方,发送方便会停止发送数据。 当接收方的窗口从 0 变为非 0 时,便会向对端发送 ack 报文,通告窗口的大小。对端收到该报文后,知道接收窗口不是 0 了,便会开始发送数据。 当通知报文在链路上丢失了, 会进行重传吗 ?不会重传。如果该报文丢失了,那么连接的两端就会死锁(发送方仍然认为接收窗口是 0,停止发送数据;接收方认为自己的通知报文已经发送出去了,已经通知了对方,自己责任已经完成),数据传输不会开启。 窗口探测定时器的作用就是应对死锁情况的补偿措施。发送方会定期发送探测报文,接收方收到探测报文之后便会回复 ack 报文,该 ack 报文同时也包含窗口信息。窗口探测定时器直到收到窗口非 0 的 ack 之后才会停止。这样就保证了即使两端发送死锁,定时器也能探测到窗口非 0 的情况,起到了兜底的作用。 窗口字段在 tcp 首部,接收侧收到报文之后,便会基于该字段更新本端发送窗口。 当 tcp 接收到 ack 报文之后,会通过函数 tcp_ack_update_window() 更新发送窗口,snd_wnd 是发送窗口,发送报文的时候会进行检查,发送的数据不会大于发送窗口。 static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack, u32 ack_seq){ struct tcp_sock *tp = tcp_sk(sk); int flag = 0; u32 nwin = ntohs(tcp_hdr(skb)->window); // 窗口扩展因子 if (likely(!tcp_hdr(skb)->syn)) nwin <<= tp->rx_opt.snd_wscale; if (tcp_may_update_window(tp, ack, ack_seq, nwin)) { flag |= FLAG_WIN_UPDATE; tcp_update_wl(tp, ack_seq); // 更新发送窗口 if (tp->snd_wnd != nwin) { tp->snd_wnd = nwin; } } return flag;} 2.3.1 定时器什么时候启动 窗口探测定时器在发送路径上启动。 void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle){ // tcp_write_xmit() 返回 true, 说明这次调用没有发送任何报文 // 则调用 tcp_check_probe_timer() 进行判断,需不需要开启窗口探测定时器 if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_mask(sk, GFP_ATOMIC))) tcp_check_probe_timer(sk);} // 判断两个条件,如果这两个条件均满足,则开启窗口探测定时器// 条件一:所有发送的数据都 ack 了// 只有这个条件满足,才会开启定时器,因为如果现在还有发送的数据没有被 ack,// 那么不需要定时器来探测,因为 ack 很快就会来了,ack 中带有窗口信息//// 条件二:窗口探测定时器没有启动。static inline void tcp_check_probe_timer(struct sock *sk){ if (!tcp_sk(sk)->packets_out && !inet_csk(sk)->icsk_pending) tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0, tcp_probe0_base(sk), TCP_RTO_MAX);} 2.3.2 定时器回调函数做什么工作 ? 窗口探测定时器超时之后调用函数 tcp_probe_timer(),在该函数中发送一个特殊的报文,对端收到该报文后便会回一个 ack,通过 ack 便可知道对端的窗口是不是已经变成非 0。 那么窗口探测报文有什么特殊之处呢 ? 特殊之处在序号,序号是已经 ack 的报文。假如本端收到的最后一个 ack 是 1000, 下一个要发送的字节序号是 1000,而窗口探测报文发送的序列号是 999。 tcp_probe_timer() 发送 0 窗口探测报文: static void tcp_probe_timer(struct sock *sk){ struct inet_connection_sock *icsk = inet_csk(sk); struct sk_buff *skb = tcp_send_head(sk); struct tcp_sock *tp = tcp_sk(sk); int max_probes; // tp->packets_out 是已发送,但是还没有 ack 的包的个数 // 如果这个数不是 0,说明最近会收到 ack,或者收不到 ack 就会重传 // 不需要窗口探测报文来探测 // !skb 说明 skb 是空,当前没有要发送的数据 // 这种情况下,也不需要探测窗口,直接返回 if (tp->packets_out || !skb) { icsk->icsk_probes_out = 0; icsk->icsk_probes_tstamp = 0; return; } // 最大重传次数 max_probes = sock_net(sk)->ipv4.sysctl_tcp_retries2; // 如果达到最大重传次数,则关闭连接 if (icsk->icsk_probes_out >= max_probes) { abort: tcp_write_err(sk); } else { // 发送窗口探测报文 tcp_send_probe0(sk); }} void tcp_send_probe0(struct sock *sk){ struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); struct net *net = sock_net(sk); unsigned long timeout; int err; // 这个函数中完成窗口探测报文的发送 err = tcp_write_wakeup(sk, LINUX_MIB_TCPWINPROBE); // 后边要重启窗口探测定时器,在重启之前,要判断一下需不需要重启 // 如下两个条件满足,则不需要重启 if (tp->packets_out || tcp_write_queue_empty(sk)) { icsk->icsk_probes_out = 0; icsk->icsk_backoff = 0; icsk->icsk_probes_tstamp = 0; return; } icsk->icsk_probes_out++; if (err <= 0) { if (icsk->icsk_backoff < net->ipv4.sysctl_tcp_retries2) icsk->icsk_backoff++; timeout = tcp_probe0_when(sk, TCP_RTO_MAX); } else { /* If packet was not sent due to local congestion, * Let senders fight for local resources conservatively. */ timeout = TCP_RESOURCE_PROBE_INTERVAL; } timeout = tcp_clamp_probe0_to_user_timeout(sk, timeout); tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0, timeout, TCP_RTO_MAX);} // 这个函数用于 0 窗口探测定时器// 同时也用于 keepalive 定时器int tcp_write_wakeup(struct sock *sk, int mib){ struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; if (sk->sk_state == TCP_CLOSE) return -1; skb = tcp_send_head(sk); // 如果当前发送队列中有报文了,并且接收窗口已经打开 // 那么就不需要发送探测报文,直接发送用户数据 if (skb && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) { int err; unsigned int mss = tcp_current_mss(sk); unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq; if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq)) tp->pushed_seq = TCP_SKB_CB(skb)->end_seq; /* We are probing the opening of a window * but the window size is != 0 * must have been a result SWS avoidance ( sender ) */ if (seg_size < TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq || skb->len > mss) { seg_size = min(seg_size, mss); TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; if (tcp_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE, skb, seg_size, mss, GFP_ATOMIC)) return -1; } else if (!tcp_skb_pcount(skb)) tcp_set_skb_tso_segs(skb, mss); TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC); if (!err) tcp_event_new_data_sent(sk, skb); return err; } else { // 发送探测报文 return tcp_xmit_probe_skb(sk, 0, mib); }} 2.3.3 窗口探测定时器什么时候停止 ? 定时器停止的情况有以下几种: ① 发送一次探测报文之后判断当前链路上是不是有已发送但是还没有确认的报文,或者发送队列中是不是有数据。上边两个条件满足其一,则不再重启定时器,也就意味着定时器后边不会再触发了。参考函数 tcp_send_probe0()。 ② 收到 ack 得知对端打开接收窗口 static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag){ // 已经发送但还没有确认的报文 int prior_packets = tp->packets_out; // 如果发送的报文都已经确认了,那么就尝试停止探测定时器 if (!prior_packets) goto no_queue; no_queue: // 这个函数中会进行判断,然后决定停止探测定时器还是重启探测定时器 tcp_ack_probe(sk); return 0;} static void tcp_ack_probe(struct sock *sk){ struct inet_connection_sock *icsk = inet_csk(sk); struct sk_buff *head = tcp_send_head(sk); const struct tcp_sock *tp = tcp_sk(sk); // 如果发送队列是空的,不对探测定时器做操作 if (!head) return; // 如果现在的窗口能把 skb 这个报文全部发送出去,则停掉探测定时器 // 否则,重启探测定时器 if (!after(TCP_SKB_CB(head)->end_seq, tcp_wnd_end(tp))) { icsk->icsk_backoff = 0; icsk->icsk_probes_tstamp = 0; inet_csk_clear_xmit_timer(sk, ICSK_TIME_PROBE0); /* Socket must be waked up by subsequent tcp_data_snd_check(). * This function is not for random using! */ } else { unsigned long when = tcp_probe0_when(sk, TCP_RTO_MAX); when = tcp_clamp_probe0_to_user_timeout(sk, when); tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0, when, TCP_RTO_MAX); }} 2.3.4 窗口探测定时器实验 为了测试 0 窗口的情况,tcp 连接建立之后,客户端向服务端发送数据,但是服务端不接收数据。这样的话,接收侧窗口很快就会变为 0。 伪码如下: 服务端: socket()bind()listen()accept_fd = accept()// 服务端 accept 一个连接之后,不立即接收报文,而是 10 s 之后再接收报文sleep(10)recv() 客户端: connect()// 客户端建立连接之后,就立即发送数据send() 抓包,如下图所示,192.168.1.104 是客户端,192.168.1.103 是服务端,建立连接之后,客户端向服务端发数据。 ① 序号 30 是发送的最后一个报文,序列号是 83313,长度是 6912,所以最后一个序列号是 83313 + 6912 - 1 = 90224。 ② 序列号 31 是服务端给客户端的 ack, ack seq 是 90225,意思是客户端下一个要发的数据序号是 90225。 ③ 序列号 34 是客户端发送的 0 窗口探测报文,可以看到序列号是 90224,而不是 90225。 ④ 序列号 35 是服务端发送给客户端的 ack, 这个 ack 中包含窗口信息,是 0 说明现在窗口仍然是 0。 过了 10s 之后,服务端开始读数据,这个时候,接收侧的窗口就打开了。 ① 43 和 44 是服务端向客户端发送的窗口打开通知。 ② 45 是客户端向服务端开始发送数据。 2.4 保活定时器 保活定时器,顾名思义,就是当 tcp 连接上长时间没有数据传输时,用来判断对端是否还存在,如果一端给另外一端发送一个保活报文,然后得到回应报文,那么说明对端就是还存在的,这条连接继续保持;反之,如果收不到对端的回应,那么就会认为对端已经不存在了,则会关闭这条连接。 保活定时器和上边的窗口探测定时器,都是探测定时器,一个是窗口探测,一个存活性探测。 保活定时器,默认是没有开启的,用户如果想使能该功能话,需要通过函数 setsockopt() 来设置 SO_KEEPALIVE 选项。 // 用户设置 KEEALIVE int val = 1;setsockopt(sock_fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&val, sizeof(val)); // SO_KEEPALIVE 选项在内核中,通过函数 tcp_set_keepalive 来完成// 可以看到,如果是打开选项,则启动定时器,关闭选项则停止定时器void tcp_set_keepalive(struct sock *sk, int val){ if ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN)) return; if (val && !sock_flag(sk, SOCK_KEEPOPEN)) inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tcp_sk(sk))); else if (!val) inet_csk_delete_keepalive_timer(sk);} 保活定时器的超时处理函数为 tcp_keepalive_timer()。 static void tcp_keepalive_timer(struct timer_list *t){ struct sock *sk = from_timer(sk, t, sk_timer); struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); u32 elapsed; // 该函数首先判断了四种情况,在这几种情况下,不需要发送保活报文,函数直接退出 // 1、套接字正在被使用,说明最近会有数据收发,所以不需要发送保活报文 if (sock_owned_by_user(sk)) { inet_csk_reset_keepalive_timer(sk, HZ / 20); goto out; } // 2、套接字处于 LISTEN 状态,处于 LISTEN 状态的套接字,不是一条连接套接字, // 也不需要发送保活报文。可以看到下边的注释,非常有趣,类似于这样的注释,内核中不少 if (sk->sk_state == TCP_LISTEN) { pr_err("Hmm... keepalive on a LISTEN ???\n"); goto out; } // 3、这个链接即将关闭,也不需要发送保活报文 // TCP_FIN_WAIT2 也会使用这个定时器 if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) { if (tp->linger2 >= 0) { const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN; if (tmo > 0) { tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); goto out; } } tcp_send_active_reset(sk, GFP_ATOMIC); goto death; } // 4、没有设置 SOCK_KEEPOPEN 标志,不发送保活报文,理论只要设置了 SO_KEEPALIVE 就会设置这个标志 // 处于关闭状态或者还在连接建立过程中,也不发送保活报文 if (!sock_flag(sk, SOCK_KEEPOPEN) || ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_SYN_SENT))) goto out; // 获取保活定时器超时时间,为了下一行代码直接 goto resched 做准备, // 如果这里不获取的话,下一句 goto resched 之后,定时器的超时时间是 0, // 很明显是不对的 elapsed = keepalive_time_when(tp); // tp->packets_out 不为 0, 说明本端发出去的包还有包没有收到 ack // 这种情况下也不发送保活报文 // write queue 不为空,说明现在连接还有数据需要传输,也不发送保活报文 if (tp->packets_out || !tcp_write_queue_empty(sk)) goto resched; // 这句代码是该函数很重要的一行代码 // tp->rcv_tstamp 是上一次收到数据的时间 // icsk->icsk_ack.lrcvtime 是上一次收到 ack 的时间 // tcp_jiffies32 是当前时间 // 该函数的返回结果就是连接上没有数据的时间, // 只有这个时间超过了 /proc/sys/net/ipv4/tcp_keepalive_time,才会发送保活报文 // 否则不发送保活报文 // static inline u32 keepalive_time_elapsed(const struct tcp_sock *tp) // { // const struct inet_connection_sock *icsk = &tp->inet_conn; // return min_t(u32, tcp_jiffies32 - icsk->icsk_ack.lrcvtime, // tcp_jiffies32 - tp->rcv_tstamp); // } elapsed = keepalive_time_elapsed(tp); if (elapsed >= keepalive_time_when(tp)) { // icsk->icsk_probes_out >= keepalive_probes(tp) // 这个条件即保活报文总数限制,默认是 9,如果超过这个数 // 则关闭连接 if ((icsk->icsk_user_timeout != 0 && elapsed >= msecs_to_jiffies(icsk->icsk_user_timeout) && icsk->icsk_probes_out > 0) || (icsk->icsk_user_timeout == 0 && icsk->icsk_probes_out >= keepalive_probes(tp))) { tcp_send_active_reset(sk, GFP_ATOMIC); tcp_write_err(sk); goto out; } if (tcp_write_wakeup(sk, LINUX_MIB_TCPKEEPALIVE) <= 0) { // 发送 keepalive 报文返回成功,增加计数 // elapsed 重新赋值,默认是 75s icsk->icsk_probes_out++; elapsed = keepalive_intvl_when(tp); } else { elapsed = TCP_RESOURCE_PROBE_INTERVAL; } } else { elapsed = keepalive_time_when(tp) - elapsed; } sk_mem_reclaim(sk); resched: inet_csk_reset_keepalive_timer(sk, elapsed); goto out; death: tcp_done(sk); out: bh_unlock_sock(sk); sock_put(sk);} ① 三个参数 tcp keepalive 功能,有三个参数可供用户配置 配置参数 默认值 作用 /proc/sys/net/ipv4/tcp_keepalive_time 7200 多长时间没有数据传输就会发送保活探测报文,默认是 7200s,即 2 个小时; 这个时间对于实际应用来说太长,可以根据应用的具体场景做调整。 /proc/sys/net/ipv4/tcp_keepalive_intvl 75 发送保活报文的时间间隔,默认是 75s;保活报文,不是发一次,收不到回应就立即认为对方不存在了,而是可以发送多次,最多可以发送的次数由下边的参数控制。 /proc/sys/net/ipv4/tcp_keepalive_probes 9 发送保活报文的次数,默认是 9,也就是说如果发送了 9 个报文,都没有收到对端的响应,那么就会认为对端不存在了。 ② 没有数据传输的时间判断 发送 keepalive 报文之前需要进行判断,其中一个条件是这条连接上多久没有数据传输了,只有没有数据传输的时间超过一定值之后,才会发送保活报文,也就是说当连接上有数据传输的时候,这条连接肯定是正常的,不需要发送保活报文。 上文中对函数 tcp_keepalive_timer(struct timer_list *t) 的注释中包括了对该时间的判断,在keepalive_time_elapsed(tp); 这行代码中获取到了没有数据活跃的持续时间。 函数 keepalive_time_elapsed() 中获取时间的方式,通过最后收到数据的时间以及最后收到的 ack 的时间来计算。乍一看是只考虑了接收方向的数据,其实不然,tp->rcv_tstamp 即最后接收到数据的时间,可以代表接收方向, icsk->icsk_ack.lrcvtime 表示最后接收到 ack 的时间,收到了 ack 说明之前肯定发送了数据,所以这个时间可以代表发送方向。 ③ 发送保活报文 tcp 中并没有一个特殊的标志来标记这个报文是保活报文,tcp hdr flag 中没有 keepalive 相关的标志,tcp 选项中也没有 keepalive 相关的选项。 那么 tcp 报文有什么特点呢 ? 发送保活报文在函数 tcp_xmit_probe_skb() 中完成。 调用关系如下: tcp_keepalive_timer() 调用 tcp_write_wakeup() 调用 tcp_xmit_probe_skb() 从函数 tcp_xmit_probe_skb() 的注释中也可以看到,这个报文的特殊之处在于序列号,序列号只一个已经发送过的序列号,并且已经 ack 过了。接收端还存在,收到这样的数据之后,会回应一个 ack 报文;如果接收端已经不存在了,那么就会发过来一个 rst 报文,本端收到 rst 报文之后便会关闭连接。 static int tcp_xmit_probe_skb(struct sock *sk, int urgent, int mib){ struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; /* We don't queue it, tcp_transmit_skb() sets ownership. */ skb = alloc_skb(MAX_TCP_HEADER, sk_gfp_mask(sk, GFP_ATOMIC | __GFP_NOWARN)); if (!skb) return -1; /* Reserve space for headers and set control bits. */ skb_reserve(skb, MAX_TCP_HEADER); /* Use a previous sequence. This should cause the other * end to send an ack. Don't queue or clone SKB, just * send it. */ tcp_init_nondata_skb(skb, tp->snd_una - !urgent, TCPHDR_ACK); NET_INC_STATS(sock_net(sk), mib); return tcp_transmit_skb(sk, skb, 0, (__force gfp_t)0);} 为了方便测试,把 keepalive 时间改成了 10s(默认 7200s),进行测试,抓包结果如下,从抓包结果可以看到: ① keepalive 的时间变成了 10s ② 在发送 keepalive 报文之前,3025 + 1072 -1 = 4096,seq 为 4096 的字节已经发送出去了,并且得到了 ack;keepalive 的序列号是 4096,本来正常的数据应该是 4097,接收方想要接收的下一个字节的编号也是 4097。 ③ 接收方收到报文之后立即回应了 ack 报文。 3 断开连接过程中的定时器 3.1 TIME_WAIT 定时器 主动发起关闭的一方,最后一个状态是 TIME_WAIT。发送最后一个 ack 之后便从 FIN_WAIT_2 状态进入到 TIME_WAIT 状态。 在函数 tcp_fin() 中处理 FIN 标志,主动断开连接的一方收到对端发送的 FIN 报文之后,返回一个 ack 之后便会进入到 TIME_WAIT 状态。tcp_time_wait() 函数中完成 TIME_WAIT 状态的处理,在这个函数中会启动 TIME_WAIT 定时器,定时器的超时处理函数 tw_timer_handler()。 void tcp_fin(struct sock *sk){ switch (sk->sk_state) { ... case TCP_FIN_WAIT2: /* Received a FIN -- send ACK and enter TIME_WAIT. */ tcp_send_ack(sk); tcp_time_wait(sk, TCP_TIME_WAIT, 0); break; ... }}