Readme:记录生活、工作、学习中自己的思考和想法,但是可能很杂乱的。目的为了提升自己BB的能力。
2023.3.9:
作为一个嵌入式(底层)开发者,通过对Linux内核的不断深入,包括VFS、内存管理、进程管理、进程调度以及网络。自以为对这些很熟悉了,但是一到要自己说出来或者被问到其中某些问题时,居然只能说一两句很表层的东西——让我十分怀疑自己是否真的熟悉了。
但仔细想想,也只是把相关书籍看了一遍,理解了一遍,并没有自己的思考或者梳理表达出来,所以上述情况也是理所当然的了。
后端开发对一些工具比如redis、mysql、docker等都是操作性的东西,但是要想面试中脱颖而出,还是要理解它们的一些原理特性,最好能够深入底层去理解。所以想转到后端开发,只是懂得如何操作是远远不够的;至少腾讯老哥是这么说的。于是除了一些什么“实战”书籍,更多的要看看一些”深入了解“的书籍。
2023.3.12:
深入理解Linux和算法可以复习这几本书,在准备跳槽面试都可以用。
Linux内核 |
《Linux内核的设计与实现》、《深入理解Linux内核》 |
Linux用户态 |
《Unix环境高级编程》 |
Linux网络 |
《Unix网络编程 I II》 |
高级算法 |
《算法导论》 |
初级算法 |
《算法》 |
2023.3.13:
在脉脉看到一张内核图,以我对Linux内核的理解,对这张图展开说一说。
从上往下看,第一个是功能性层,对Linux进行一个按功能划分为几个大的模块;第二个是用户接口层,Linux对用户空间提供的接口;第三个虚拟层,Linux提供了许多虚拟的功能;第四个是桥层,是一些起到跨层功能的辅助模块;第五个是逻辑层,是上述各种功能模块的实现层面;第六个是设备控制,控制底层硬件设备的层面;第七个是硬件接口层,对各个硬件的操作做了一个访问的接口,即驱动;第八个就是电子层,真正的硬件(控制器)。
从左往右看,就是按模块地分别研究上述各个层面的内容,包括用户接口、系统接口、进程、内存、存储、网络。这几个模块就几乎包含了内核的所有东西了。模块内各个层面的内容有什么、以及各个模块之间的联系也画了出来,现在尽量地按模块从上到下分析一下整张图。
首先是用户接口。在用户空间层面,用户对(硬件接口HI)字符设备(文件)的操作就是一类用户接口;对各种设备的操作之前首先要通过cdev_add添加一个字符设备以及操作(这个只是用户空间的接口,实际添加操作是调用相应的系统调用实现),比如输入设备(键盘)、控制台终端、视频播放(没了解过)、系统日志文件等等,这些都是字符设备。添加以后,我们尽管调用接口对它们进行操作就行。用户接口在虚拟层实现了一个安全子模块,没了解过。在桥层做了一些调试用的内核接口,如常见的prink,图中其它列出来的就不了解。字符设备等在逻辑层对应一个硬件接口子系统,比如熟悉的tty,其它就不了解。再接着是设备控制层的,Linux封装了各种设备为抽象设备,并且分类,每类都有相对应的驱动类,用来辅助查找相对于的驱动程序,在drivers/目录下可以看到。再接下来是硬件接口层,包含一些外围设备的驱动,Linux内置了许多外围设备的驱动了。通常是比较重要的设备。最后是电子层,对用户接口来说,包括键盘、摄像头、鼠标、显卡、声卡等等。
第二个模块是系统或者系统接口,描述了Linux内核比较核心的一些内容。在用户空间接口,系统提供了系统调用接口,包括访问文件、访问设备等资源;系统还提供了一些系统文件,允许用户空间的程序共享资源、通信以及通过操作这些文件来操作设备。系统提供用户空间层的各种接口目的为了限制用户空间程序访问内核资源,避免其对内存、资源设备进行破坏。设备有各种各样的类,而提供给用户空间的接口比较统一,所以需要一个抽象的接口去对接用户态和内核态的接口,这样才能实现统一。因此图中在虚拟层,系统实现了一个以kobject为核心的设备树......总之按设备类型分类,组织成树的形状,这样,Linux就可以很优雅的管理各种类型的设备;sysfs就是表示系统中设备树的一个文件系统。桥层就是实现将上述的设备以对象的形式注册到设备控制层。对象提供面向对象的操作,即我们可以像操作一个对象一样操作一个设备;壳层还提供了模块的概念,就是以.ko结尾的文件,可以加载和卸载到Linux内核中运行。系统的逻辑层实现了系统引导、电源管理,包括开关操作,这些操作进而会影响到内核进程的一个运行状态。系统的设备控制层提供了通用的硬件访问,不是很了解。硬件接口层就是提供了设备访问点和总线驱动,这些接口就是直接访问硬件,包括I/O端口、内存I/O、USB控制器、PCI控制器等等,这些都是内核内置的驱动。
剩下四个模块待写......
2023.3.14:
下一个模块是进程。进程是CPU的核心程序,Linux将要运行的程序以进程的形式管理。用户空间程序利用内核提供的系统调用或者C标准库的提供接口,可以进行进程的管理,包括运行、派生、杀死、传递信号等等操作。在抽象的操作系统概念中,进程是操作系统的资源的分配单位,而线程是调度的基本单位,线程运行于进程之上并被进程管理。而Linux实现线程比较简单,把所有的线程当作进程实现,而同一个进程的线程有一个共同的进程描述符,每个线程共享进程的资源。Linux实现线程很优雅,有多少个线程就创建多少个task_struct,然后指定这些进程共享的资源即可。如图中所示,线程在虚拟层实现,包括有关工作任务(也就是进程)调度、线程创建等等操作;有关全局变量如current也在此定义。在桥层实现一些用于进程同步的接口,控制进程的状态和在调度器中的行为,包括定时器、一些锁:互斥量、条件变量、信号量、自旋锁;还有事件等待。进程模块的核心是进度调度管理,调度器在逻辑层实现。调度器根据一些调度操作来对进程做一些状态的设置,然后提交到CPU中去执行相应的操作:运行、睡眠、挂起、阻塞、死亡。在设备控制层实现了中断的核心,包括我们常用的jiffies,定时器维护、时间中断维护以及执行中断程序、软中断。上述均是对各种CPU进行抽象的描述以及定义同一个接口,而Linux内核要做的是将这些操作传递给具体的CPU执行。硬件接口层提供了具体的CPU接口,更准确的说是何种架构,比如x86或者arm架构,架构不同,调度、中断的一些细节不同,Linux为不同的架构实现了不同的代码做了大量的工作,我也看不懂。最后在电子层就是运行我们操作系统的CPU了,在多处理器的时代,硬件CPU不止一个。
再下一个模块是内存管理,面试中问得最多又答不上来的内容——谁会去做如此复杂的内存管理呢?只能背一背事后又忘记罢了。Linux内核对内存的管理涉及到不同的地址、不同的空间,在内核的各种接口中用到的内存可能是虚拟的、也可能是物理的,但是不会明确的告诉你,需要你联系上下文去理解——可谁这么有空去研究这座内核大山呢。好在一些资深内核专家写的书籍,让我们站在上帝的视角去理解Linux的内存管理原理。
Linux内核涉及到几种类型的地址,总结如下:
用户虚拟地址(User virtual addresses) |
这是用户空间程序所能看到的常规地址。用户地址或者是32位的,或者是64位的,这取决于硬件的体系架构。.每个进程有它自己的虚拟地址空间. |
物理地址(IPhysical addresses) |
该地址处理器和系统内存(即内核)之间使用.长度32或64位,在某些情况下甚至32位系统也能使用64位的物理内存。 |
总线地址(Bus addresses) |
该地址在外围总线和内存之间使用.通常它们与处理器使用的物理地址相同,但这么做并不是必须的。一些计算机体系架构提供了I/O内存管理单元(IOMMU),它实现总线和主内存之间的重新映射。IOMMU可以用很多种方式让事情变得简单(比如使内存中的分散缓冲区对设备来说是连续的),但是当设置DMA操作时,编写IOMMU相关的代码是一个必须的额外步骤。当然总线地址与体系架构密切相关的。 |
内核逻辑地址(Kernel logical addresses) |
内核逻辑地址组成了内核的常规地址空间。该地址影射了部分(或全部)内存,并经常视为物理地址。在大多数体系结构中,逻辑地址和与其相关联的物理地址的不同,仅仅是它们之间存在一个固定的偏移量。逻辑地址使用硬件内建的指针大小,因此在安装大量内存的32位系统中,它无法寻址全部的物理地址。逻辑地址通常保持在unsigned long或者void *这样类型的变量中。kmalloc返回的内存就是内核逻辑地址。持在unsigned long或者void *这样类型的变量中。kmalloc返回的内存就是内核逻辑地址。 |
内核虚拟地址(Kernel Virtual addresses) |
内核虚拟地址和逻辑地址的相同之处在于,它们都将内核空间的地址映射到物理地址上。内核虚拟地址与物理地址的映射不必是线性的和一对一的,而这时逻辑地址空间的特点。所有的逻辑地址都是内核虚拟地址,但是许多内核虚拟地址不是逻辑地址。举个例子,vmalloc分配的内存具有一个虚拟地址(但并不存在直接的物理映射)。kmap函数也返回一个虚拟地址。虚拟地址通常保存在指针变量中。射)。kmap函数也返回一个虚拟地址。虚拟地址通常保存在指针变量中。 |
回到图中,在用户空间访问内存的接口有几种。在/proc/meminfo这个文件可以读到有关内存的信息,包括缓存的、分配器分配的、页大小、交换内存等等;用户态进程用到的内存共享操作mm还有其它等等,不怎么了解。用户态的进程拥有自己独立的虚拟地址空间,当进程运行的时候,会根据程序来具体分配内存,这个内存分配的过程其实就是将用户进程的虚拟地址空间映射到内核虚拟地址空间中去。映射则需要借助页表去保存起来,以在程序在CPU运行时转换为真正物理地址。具体会分配的内存包括:程序代码本身的空间(代码段)、已初始化的全局变量(数据段)、未初始化的全局变量(两种全局变量的区别是后者会被映射到零页,前者正常映射并需要带数据)、程序栈、如C的动态库各个数据段、通过malloc(匿名)分配的空间、共享内存段、内存映射文件(虚拟内存),这些映射信息都会存放在进程的页表信息中。为了方便说明内存管理过程,这里从底向上说起。在电子层中,内存具体可以分为两种,RAM和DMA,前者是正常使用内存,后者是用于直接内存访问;这两种内存都由内存本身的管理单元管理。而Linux内核以页为单位管理内存,一页通常为4KB;每一页都会用一个结构体page_struct管理,这个结构体主要记录这个页的虚拟地址和物理地址,还有一些记录信息。所有页组成一个物理地址空间,Linux内核一般按架构把它分类不同的区;按x86-32架构来说,简单分为三个区:DMA区、NORMAL区、高端区。按4G物理空间来说,地址范围分别是(0-16MB,16MB-896MB,896MB-4GB);第一个区只能用于DMA,第二个区内核正常可直接寻址的分区,第三个用于映射的区,即使用这个范围的地址空间时,需要做一个映射才能访问,具体来说就是将这个范围的物理空间映射到内核的虚拟地址空间。在硬件接口层,内存模块实现了不同架构的具体内存操作相关接口,包括分配、回收、映射、初始化等。在设备控制器层,内存模块实现了通用的内存页分配器。最基本的的分配接口是页分配和页释放,这个接口会分配一个物理页或释放一个物理页,即前面所说的page_struct,分配时候,我们可以许多相关的标志,比如用哪个区的、是否可睡眠、是否初始化、共享等等。通常我们需要分配一段连续的空间,内核提供了一个slab分配器,可以根据大小分配所需内存。它是智能的,比如它知道在频繁释放和避免碎片的矛盾中折中;它知道内核页大小、高速缓存、对象大小,可以做出更合理的管理操作。在逻辑层,Linux内核的另一个分配器kmalloc就是基于slab分配器的实现。在桥层,实现了一个映射器,就是把高端区的地址映射到内核虚拟地址空间中。这里说明一下内核的逻辑地址空间,如上面的地址类型介绍,内核的逻辑地址和物理空间地址就是差一个固定偏移量,在内核分析一个地址它属于哪种,皆有可能。在逻辑层提供了具体的映射接口,最基本的是内存映射接口kmap,每次映射一个页。对于需要映射字为单位的内存,可以用内存分配器vmalloc。在用户空间层,需要获取内存,可以使用系统调用接口或者相关的C函数库。
这里需要说明一下,以上的讨论没有涉及到内存交换,从图中可以看到,实际MMU提供缺页中断的处理;而内存交换即虚拟内存映射只在用户态层即用户空间进程存在,即把硬盘空间(文件)映射到了用户虚拟地址空间(作为内存)。下一个模块会涉及到相关操作。
啊,还有两个模块。。。
2023.3.15:
程序员需要不断看技术书籍,进行技术沉淀——但是发现自己沉淀出来的东西都比较浅显,如何改进呢......
2023.3.16:
存储模块主要是文件、块设备相关的操作和管理。先看看用户空间层实现了什么东西。图中展示了文件和目录的访问 的一些接口,包括打开、读写的系统调用。然后非阻塞I/O的系统调用、异步I/O等等。文件使用文件描述符fd表示,所有文件操作的接口的对象就是它。图中的什么向量I/O没听说过,还有0复制也没怎么了解。接下虚拟层实现了一个虚拟文件系统(VFS),Linux所有的文件系统都的操作基于VFS实现,也就说明我们对任何类型的文件系统中的文件读写访问都可以使用相同的接口。在Linux内核2.5版本(没记错的话)之后,VFS增加了一个通用块层的实现,提高了VFS的效率以及通用性。其流程简单说一下就是,当我们读一个文件,首先调用一个通用函数,如read(),接口read会根据该文件类型调用相应的系统调用vfs_read(),该系统调用会对所读的内容按块进行拆分,传入通用块层中,将所读的范围封装成bio,接着将其传入队列调度层,尽可能的合并不同的bio中相邻的块,具体取决于调度策略;最后将bio提交给文件所在块设备的驱动中,由驱动去完成对硬件的读。在桥层,Linux内核用内存实现了一个对文件的高速缓存,加快文件的读写。缓存的刷新策略也有几种,刷新的执行每个缓存有单独的线程去执行;网络文件系统的缓存也在桥层实现;对于实现了虚拟内存的系统,其映射到文件的部分内存,当发生缺页时,中断程序会把所需要的映射文件交加载到用于内存交换的物理内存所在区域,替换暂时不用的内存映射文件(如果有),这就叫做内存交换。在逻辑层实现的是每种文件系统的文件操作,每种文件系统都对应一个逻辑文件系统,它们定义了自己的文件操作对象。设备控制层就是刚刚所说的通用块层,这里每个硬盘都对应一个块设备,块设备可以拥有自己的请求队列、操作对象;Linux实现了一种通用的块设备系统SCSI,支持多种硬盘,即插即用等。硬件接口层就是具体的块设备驱动,scsi设备、nvme设备均被Linux内置了驱动,可即插即用。在电子层,对应的是各种存储设备的控制器,包括nvme、SCSI、SATA等。
网络模块也是一个相当复杂的东西。网络可以用于进程间的通信,包括单个主机之间、不同主机之间的进程通信,Linux在这两种的实现上有些不同,主要区别是报文经不经过网卡(即网卡控制器),这个区别了解比较少。在用户空间层,我们使用语言的标准库去实现网络编程,Linux C标准库也有相应的接口,这些接口对应着一些系统调用。通信的过程涉及到的操作有:创建套接字、创建套接字描述符、连接两个套接字(描述符)、监听套接字、接受连接、发送报文、接收报文等。我们还可以在/proc/net/目录下查看或操作有关网络的配置。套接字是描述IP地址和端口的对象,一个套接字对应一个通信对象。由于IP地址类型有两种,IPv4和IPv6,即两种地址簇。因此,当创建套接字时,我们需要声明套接字的类型。在虚拟层,Linux实现了可以操作不同地址簇的套接字的接口,方便网络编程。桥层实现了网络文件系统的缓存的操作以及套接字片(没了解过)。逻辑层实现了不同协议的操作接口,包括ipv4、tcp、udp等等。设备控制层实现了网络接口控制,包括队列、网口注册等等,一个主机可以有多个网口。接下来的硬件接口层就是网络设备的驱动。电子层就是网络控制器(网卡),如以太网网卡、WIFI网卡。
全图终。
2023.3.22:
这里要纠正一下上面对虚拟内存、内存映射文件的一些理解错误。一个是内核空间不包括交换分区,第二个是内存映射文件和交换分区没有直接关系;第三个内核也有缺页中断,一个是vmalloc_fault,另一个原因是内存置换的存在,内核内存也可能被置换出去(chatGPT说的)。
SSD需要擦除才能写,擦除的单位是块,但是这个块可以有多个页,所以会带来写放大问题——写一个页可能要擦除多个页。另外SSD垃圾回收也就是提前对无效区进行擦除,因为垃圾回收可能要进行数据搬移,因为一个块不一定所有的页都是无效。冷热数据分流可以减少垃圾回收的数据搬移。另外磨损均衡可以让每个块擦除达到一个平衡次数。
2023.3.31:
3月底了,来个总结。一个月时间复习了Linux内核的不少知识,但是挺多都容易忘了,或者想起来的内容也不完整,甚至记串内容。这个根本原因还是没有理解深入吧,毕竟Linux内核内容这么多,了解了解也足够了,又不是从事内核开发。
Linux C语言工程师貌似除了熟悉操作系统的一些东西,貌似对TCP/IP协议也要精通。
TCP/IP协议是一种网络协议簇,作为网络上的主机通信的协议。它是一种七层或者四层协议,每层之间是独立的,上层为下层提供服务;每一层也有多种协议......对于四层协议——应用层、运输层、网络层和数据链路层来说,核心协议有TCP、UDP和IPv4、IPv6协议、MAC协议;前两个是运输层的协议,其报文结构核心是源端口号和目标端口号,这是标记一个主机的进程的功能;而后两个是网络层的协议,和报文结构核心是源IP地址和目标IP地址,这是标记一个主机(网卡)的功能。最后一个是数据链路层的协议,它是报文到达了目标主机的所在的链路层网络的最终送达规则。它决定了路由器或者交换机最终将这个报文转发给哪个接口。当然,MAC协议在一些情况是不需要的,比如广播报文或者有些转发器直接将这个报文转发给所有接口。MAC协议的出现改变了转发器的一些弊端,比如将报文直接转发给所有接口。MAC协议的关键是MAC地址和转发表的自动建立。MAC地址是一个网卡的硬件唯一地址,即起到标记的作用。
说一说TCP/IP协议在Linux中是如何实现的。TCP/IP在Linux中通过套接字来实现这些协议,套接字记录了两个关键信息:端口号和IP地址。然后通信的两个进程就可以根据所需调佣Linux的网络接口进行通信,只要设置相关的参数就可以进行TCP通信或者UDP通信了。
只能回忆起这些了——在没有任意问题提示的情况下...
2023.5.10:
虽然SSD通常有自己的FTL,但是如果使用SSD作为逻辑设备,写入的单元大小是4k或者大于4k的情况,有自己的FTL管理可以使得性能更好(重定向和垃圾回收等)。
2023.5.31:
spring、node.js等是一个完成应用程序开发框架,可以开发一个完整PC、Web、移动端的应用程序;而不仅仅限于服务器,服务器只是他们一部分;而nginx、apache等是具体的服务器,可用于代理、 负载均衡、静态服务器等等。
2023.7.14:
C++STL中,关于容器有两大类,序列式容器和关联式容器。 序列式容器包括:数组Array、向量Vector、链表List、单向链表Forward_list、双向队列Deque;关联式容器包括:有序的:集合Set、映射Map,无序的:Unordered_set、Unordered_map。
在实现上,对于序列式容器,数组是不可扩容的C式数组——类型为类;Vector是会自动扩容的数组,扩容策略为两倍于当前;以上两种都提供索引下表访问功能,但没有异常检查,而有异常检查的成员访问接口是at;链表是双向链表,提供链式存储,修改和插入元素一般情况下都是常量复杂度;单向链表是类似双向链表限制为单向,操作上有些限制,但标准库并没有规定其实现基于双向链表;双向队列是可以从容器的两端插入和删除元素,其实现和vector类似,但是不支持自动扩容,只能手动扩容或缩减。
对于关联式容器的有序容器,Set和Map都会自动对元素进行排序存放,根据实现库中的这些容器操作的复杂可以发现他们一般都是用二叉平衡树实现这些容器,实际上更多的是使用红黑树;而无序关联式容器则使用哈希函数实现,并且将元素存储到bucket中;这些关联式容器不支持存储重复元素,要支持的话,使用Multi-版本。可以从容器的能力、操作以及异常处理三个方向去分析他们的特点,适用的场景等等;所有的容器都有共通的能力或操作,这使得操作起他们们时可以有比较统一的操作,另外通过迭代器,除了一些容器的特殊自有算法(如set、map的访问、查找接口),他们的一些算法可以统一实现,包括非更易型算法、更易型算法、移除型算法、变序型算法、排序算法等。