龙空技术网

C/C++后端工程师的圣地—内核殿堂(含学习路线)

嵌入式Linux内核 104

前言:

此刻咱们对“游戏c语言后端框架是什么”都比较关心,朋友们都需要了解一些“游戏c语言后端框架是什么”的相关文章。那么小编也在网络上收集了一些关于“游戏c语言后端框架是什么””的相关文章,希望姐妹们能喜欢,你们一起来学习一下吧!

一、Linux内核组成部分

其中可以看到三个层次:用户空间、内核空间、硬件。

用户程序通过陷入完成由用户态到内核态的转换。系统调用作为用户级与内核级交互方式,分为2部分:与文件子系统的交互和与进程控制子系统的交互。

文件子系统管理文件。包括:分配文件空间、管理空闲空间、控制对文件的存取以及为用户检索数据。

进程控制子系统负责进程同步、进程间通信、进程调度和存储管理。

Linux内核主要由5个子系统组成:进程调度、内存管理、虚拟文件系统、进程间通信和网络接口。

1.进程调度(SCHED):用来负责控制进程对CPU资源的使用。

2.内存管理(MM):用于确保所有进程能够安全地共享机器主内存区。

3.虚拟文件系统(VFS):为上层应用程序提供统一的接口。

4.进程间通信(IPC):用于支持多种进程间的信息交换。通过系统调用实现进程间的信息交换。

5.网络接口(NET):提供多种网络通信标准的访问并提供对多种网络硬件的支持。

所有的模块都要通过进程调度来运行。

1.1Linux 内核的作用是什么?

内容有以下四项作用:

内存管理:追踪记录有多少内存存储了什么以及存储在哪里进程管理:确定哪些进程可以使用中央处理器(CPU)、何时使用以及持续多长时间设备驱动程序:充当硬件与进程之间的调解程序/解释程序系统调用和安全防护:从流程接受服务请求

正确实现时,内核对用户是不可见的,它在自己的小世界(称为内核空间)中工作,从中分配内存,跟踪所有内容的存储位置。用户看到的东西(比如Web浏览器和文件)叫做用户空间。这些应用程序通过系统调用接口(SCI)与内核交互。

可以这样理解:内核就像一个忙碌的私人助理,为高管(硬件)服务。助理的工作是将员工和公众(用户)的信息和请求(流程)传递给高管,记住存储的内容和位置(内存),并确定谁可以在任何给定的时间访问高管,以及会议时间有多长。

1.2学习Linux内核准备工作:熟悉C语言,这个是最基本的了解编译连接过程,如果写过ld、lcf类的链接文件最好,这样就能理解类似percpu变量的实现方法学过或者自学过计算机组成原理或者微机原理,知道smp、cpu、cache、ram、hdd、bus的概念,明白中断、dma、寄存器,这样才能理解所谓的上下文context、barrier是什么

还没看懂,没关系,点击这里

资料直通车:最新Linux内核源码资料文档+视频资料

内核学习地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

二、怎么阅读内核

目的:为了更好地编写驱动程序,对自己写的程序有更深入的理解,粗自己的岗位定位在底层。

获取内核源码:(后台私信内核 领取懒人资料包)

Linux庞大而复杂,其核心包括进程管理、内存管理、网络、文件系统和arch-entity="2 " >驱动,这些都依赖于内核提供的各种库和接口、各种内核机制以及arch下可能对应的汇编。没有这些基础,要流畅的阅读代码就有点困难了。

Linux的代码量很大,而且是在gcc的基础上开发的,针对各种场景做了大量的优化。所以第二件事就是要熟悉gcc下C的扩展用法,要有一个好的代码查看工具。推荐源洞察。

内核运行在特定的硬件平台上,所以对于底层涉及的部分有不同的arch实现,包括大量的汇编操作,所以以arm为例。如果想研究内核相应部分的代码,就必须多读,熟悉arm的官方文档。

而且代码和资料基本都是英文的,一般词汇和专业词汇都有,所以英语基础好很重要。这个没有捷径,就是多读书,当然也有积累的方法,后面会讲到。

每个模块都有很多细节。可能你年轻的时候记性好吧。你开发一个模块的时候,都读了好几遍了,所有的细节都不是很清楚。可能3、5年后再看就很难记住了,所以需要想办法形成积累。否则可能会忘记看,辛苦又低效。

内核编程有自己的风格和一些公认的规则,尤其是命名、排版、代码文件分类等。有些可能不符合规则,也可能很好,但如果大家都这样用,那自然就是所谓的艺术了,熟悉这些艺术有助于举一反三的学习其他模块的代码。

内核的代码量与日俱增,模块也越来越复杂,所以可维护性对于内核来说也是非常重要的。所以在如何更有利于以后的维护上做了很多努力。内核是操作系统的核心部分,其稳定性和性能自然非常重要。它用了很多技巧来应对。研究这些,积累起来,有利于进一步理解其原理。

2.1阅读Linux内核

常用下面两种方法:

bochs+linux0.11+书( linux内核完全注释、linux内核完全剖析、 linux内核设计的艺术 )sourcesinsight+linx2.X+书( linux内核情景分析)

注:阅读源码分为纵向阅读和横向阅读,纵向就是跟着内核的执行流程来读,横向就是按照内核的各大功能模块来读。

第一种方法纵向或者横向来读都可以,因为代码量不是很大。《linux内核完全剖析》 《linux内核完全注释》 是引导你横向阅读的书,《linux内核设计的艺术》引导你纵向阅读的书。个人经验可以横向纵向结合着来,纵向跟着bochs调试工具来是必不可少的,当遇到问题时进入到相应的功能模块横向拓展一下。

《linux内核情:景分析》中的内核版本是2 .4.X ,现代内核版本还是推荐横向阅读,纵向几乎不可能。我在Linux 下搭建了quem虚拟机,然后用GDB调试内核也可以。总之阅读源码的方法也就上面两种,贵在坚持,但是别闭门N久学内核,没有意义。而且长时间只读代码,不敲代码是不行的。

如果想在简历中写上关于linux内核的经验,先不要花大量时间看源码,先把《linux内核设计与实现》读了, 在找工作中更有用。

2.2通常Linux会有以下目录arch子目录包括所有和体系结构相关的核心代码。它还有更深的子目录,每一个代表一 种支持的体系结构include子目录包括编译核心所需要的大部分include文件。它也有更深的子目录,每一个支持的体系结构。include/asm 是这个体系结构所需要的真实的include目录的软链接,例如include/asm-i386。为了改变体系结构,你需要编辑核心的makefile , 重新运行Linux的核心配置程序init这个目录包含核心的初始化代码,这时研究核心如何工作的一-个非常好的起点mm这个目录包括所有的内存管理代码。和体系结构相关的内存管理代码位于arch/*/mm/drivers系统所有的设备驱动程序在这个目录。它们被划分成设备驱动程序类ipc这个目录包含核心的进程间通讯的代码modules这只是- -个用来存放建立好的模块的目录fs所有的文件系统代码。被划分成子目录,每一个支持的文件系统一个kernel主要的核心代码。同样, 和体系相关的核心代码放在arch/*/kernelnet核心的网络代码lib这个目录放置核心的库代码。和体系结构相关的库代码在arch/*/ib/scripts这个目录包含脚本(例如awk和tk脚本),用于配置核心

按照以下顺序阅读源代码会轻松点:

核心功能(kernel)内存管理(mm)文件系统(s)进程通讯(ipc)网络(net)系统启动和初始化(init/main和head.S)其他等等2.3具体分类说明

1.系统的启动和初始化

在基于Intel的系统上,当loadlin exe或LILO把内核装入到内存并把控制权传递给内核时,内核开始启动。关于这-部分,看arch/i386/kernel/head.S, head. S进行特定结构的设置,然后跳转 到initmain.c的main()例程。

2.内存管理

内存管理的代码主要在/mm ,但特定结构的代码在arch/*/mm。缺页中断处理的代码在mm/memory.c ,而内存映射和页高速缓存器的代码在mm/ilemap.c。缓冲器高速缓存是在mm/bufer.c中实现,而交换高速缓存是在mm/swap_ _state.c 和mm/swapfile c中实现。

3.内核

内核中,特定结构的代码在arch/*/kernel ,调度程序在kerel/sched.c , fork的代码在kernel/fork.c , task_ struct 数据结构在include/linux/sched.h中。

4. PCI

PCI伪驱动程序在drivers/pci/pcic , 其定义在include/linux/pci.h。每一种结构都有一 些特定的 PCI BIOS代码,InteI的在arch/alpha/kernel/bios32.c.

5.进程间通信

所有System V IPC对象权限都包含在ipc_ perm 数据结构中,这可以在include/inux/ipc .h中找到。System V消息是在ipc/msg.c中实现,共享内存在ipc/shm.c中,信号量在ipc/sem.c中,管道在ipc/pipe.c中实现。

6.中断处理

内核的中断处理代码是几乎所有的微处理器所特有的。中断处理代码在arch/i386/kernel/irq.c中,其定 义好在include/asm-i386/irq.h中。

7.设备驱动程序

Linux内核源代码的很多行是设备驱动程序。Linux设备驱动程 序的所有源代码都保存在/driver ,根据类型可进一步划分为 :/block块设备驱动程序如ide(在ide.c)。

如果你想看包含文件系统的所有设备是如何被初始化的,你应当看drivers/block/genhd.c中的device_ setup() , device_ setup()不仅初始化了硬盘,当一 一个网络安装nfs文件系统时,它也初始化网络。块设备包含了基于IDE和SCSI的设备。/char这 是看字符设备(如tty,串口及鼠标等)驱动程序的地方。/cdrom Linux的所有CDROM代码都在这儿,如在这儿可以找到Soundblaster CDROM的驱动程序。

注意ide CD的驱动程序是ide-cd.c ,放在drivers/block , SCSI CD的驱动程序是scsi.c ,放在drivers/scsi。/pci这 是PCI伪驱动程序的源代码,在这里可以看到PCI子系统是如何被映射和初始化的。/scsi 在这里可以找到所有的SCSI代码及Linux所支持的scsi设备的所有设备驱动程序。/net在这里可以找到网络设备驱动程序,如DECChip 21040 PCI以太网驱动程序在tulip.c中。/sound 这是所有声卡驱动程序的所在地。

8.文件系统

EXT2文件系统的源代码全部在fs/ext2/目录下,而其数据结构的定义在include/linux/ext2_ fs.h, ext2_ fts_ i.h及 ext2_ _fs_ _sb.h中。 虚拟文件系统的数据结构在include/inux/fs .h中描述,而代码是在fs/*中。缓冲区高速缓存 与更新内核的守护进程的实现是在fs/buffer.c中。

9.网络

网络代码保存在/net中, 大部分的include文件在include/net下,BSD套节口代码在net/socket.c中, IP 第4版本的套节口代码在netipv4/af_ inet.c。- 般的协议支持代码(包括sk_ buff 处理例程)在net/core下, TCP/IP联网代码在netipv4下,网络设备驱动程序在/drivers/net下。

10.模块

内核模块的代码部分在内核中, 部分在模块包中,前者全部在kemel/modules.c中,而数据结构和内核守护进程kerneld的信息分别在include/inux/module.h和include/linuxkerneld.h中。如果你想看ELF目标文件的结构,它位于include/linuxelf.h中。

三、Linux内核体系3.1进程管理

进程的定义

进程是指一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。其如下图所示:

由上图可以知道,进程包含了正在运行的一个程序的所有的状态的信息,其主要包括以下:

代码数据状态寄存器,例如CPU的状态,栈指针,PC指针等通用寄存器进程占用系统资源,打开文件,已分配的内存信息等

进程的特点

动态性:可以动态创建、结束进程。并发性:进程可以被独立调度并占用处理器运行。独立性:不同进程的工作不相互影响。制约性:因访问共享数据/资源或进程间同步而产生制约。

进程与程序的联系

1.进程是操作系统处于执行状态程序的抽象

程序 = 文件 (静态的可执行文件)进程 = 执行中的程序 = 程序 + 执行状态

2.同一程序的多次执行过程对应为不同的进程

如命令ls的多次执行对应多个进程

3.进程执行是需要资源

内存:保护代码和数据CPU:执行指令

进程与程序的区别

进程是动态的,程序是静态的

程序是有序代码的集合进程是程序的执行,进程有核心态和用户态

进程是暂时的,程序是永久的

进程是一个状态变化的过程程序可长久保存

进程与程序的组成不同

进程的组成包括程序、数据和进程控制块

进程控制块(PCB)

进程是操作系统中调度的一个实体,需要对进程所拥有的资源进行抽象,这个抽象的形式就是进程控制块(PCB),主要是用来管理控制进程运行所用的信息集合。

操作系统用PCB来描述进程的基本情况以及运行变化的过程PCB是进程存在的唯一标志,每个进程都在操作系统中一个对应的PCB

对于进程控制块需要描述以下信息:

进程的运行状态:包括就绪、运行、等待阻塞、僵尸等状态程序计数器:记录当前进程运行到哪条指令CPU寄存器:主要用于保存当前运行的上下文,记录CPU所有必须保存下来的寄存器信息,以便当前进程调度出去之后还能调度回来并接着运行CPU调度信息:包括进程优先级、调度队列和调度等相关信息内存管理信息:进程使用的内存信息,如进程的页表等统计信息:包含进程运行时间等相关统计信息文件相关信息:包括进程打开的文件等

因此,进程描述符是用于描述进程运行状态以及控制进程运行所需要的全部信息,是操作系统用来感知进程存在的一个非常重要的额数据结构。任何一个操作系统的实现都需要一个数据结构来描述进程描述符,所以linux内核采用一个名为task_struct的结构体,该内容后面详细学习。

对于进程的上下文切换,如下图所示:

进程主要是通过中断或者系统调用进入内核

上下文保存在对应的PCB中,被调度时,从PCB取出上下文并恢复进程

进程的生命周期

一个进程的生命周期可以划分为一组状态,这些状态刻画了整个进程。进程状态即体现一个进程的生命状态。进程的整个生命周期如下:

进程创建

在linux系统中,许多进程在诞生之初都与其父进程共同用一个存储空间。但是子进程又可以建立自己的存储空间,并与父进程“分道扬镳”,成为与父进程一样真正意义上的进程。

linux系统运行的第一个进程是在初始化阶段“捏造出来的”。而此后的线程或进程都是由一个已存在的进程像细胞分裂一样通过系统调用复制出来的,称为“fork()”或者“clone()”引起进程创建的情况。

系统初始化创建,例如第一个进程用户请求创建一个新进程正常运行的进程执行创建进程的系统调用

进程执行

进程的创建解决了从无到有的过程,而当进程创建后,就处于就绪状态,进程具备运行条件,等待系统分配处理器以便运行。

而进程的执行是内核选择一个就绪的进程,让它占用处理器并执行,主要是从就绪状态切换到运行态的过程,但是这里面涉及到如何选择哪个就绪队列,采用何种算法,这个后面会单独学习。

进程等待

指进程不具备运行条件,正在等待某个事件的完成,此时进程进入等待(阻塞)的状态,其有如下情况

请求并等待系统服务,无法马上完成启动某些操作,无法马上完成需要的数据没有到达

只有进程自身才知道何时需要等待某种事情的发生,该过程只能发生在运行态到等待态的。

进程抢占

进程抢占可能发生在两种情况下:

更高优先级的任务进入TASK_RUNNING状态当前进程的时间片到期

无论进程当前运行在内核态还是用户态,都可以发生抢占,被抢占的进程仍然运行在TASK_RUNNING状态。

进程唤醒

唤醒进程的情况

被阻塞进程需要的资源可以满足被阻塞进程等待的事件到达

进程只能被别的进程或操作系统唤醒

进程结束

进程结束的情况

正常退出(自愿的)错误退出(自愿的)致命错误退出(强制性的)被其他进程所杀(强制性的)

我们来看看sleep系统调用对应的进程状态的变化情况,如下图:

首先,创建sleep进程,进程处于就绪状态,等待操作系统调度当操作系统从就绪态开始执行该进程,就处于运行态运行态去等待time硬件时间到,系统马上就切换到等待态,并切换到另外的进程运行当硬件时间到后,该进程马上就回到就绪,等待操作系统运行当再次运行完毕后,该进程就退出

我们来实现看看进程切换的完成状态图,如下图示:

进程的状态模型

启动: 从NULL->创建的过程,一个新进程被产生出来执行一个程序创建->就绪: 当进程被创建完成并初始化后,一切就绪准备运行时,就变成就绪状态就绪->运行: 处于就绪状态的进程被进程调度程序选中后,就分配到处理机上来运行运行->结束: 当进程表示它已经完成或者因某种原因出错,当前运行进程会由操作系统结束处理运行->就绪: 处于运行状态的进程在其运行过程中,由于分配给它的处理机时间片用完而让出处理机运行->等待: 当进程请求某资源且必须等待等待->就绪: 当进程要等待某事件到来时,它从阻塞状态变成就绪状态

进程挂起模型

每次执行中的进程必须完全载入内存中,因此所以队列中的所有进程必须驻留在内存中。内存中保存多个进程,当一个进程正在等待,处理器可以转移到另外一个进程,但是CPU比I/O要快很多,以至于内存中所有进程都在等待I/O的情况就显得很常见。那么我们该如何处理这种问题呢?

该问题产生的直接原因是内存问题,那么最直接的方法是扩充内存适应更多的进程另外一种方式,是虚拟内存技术的方式,采用交换,把内存中某个进程的一部分或者全部移到磁盘中。当内存中没有处于就绪状态的进程时,操作系统就把阻塞的进程换到磁盘中的“挂起队列”中。

不同于进程阻塞。挂起时没有占用该内存空间,而是映像在磁盘上,主要是减小进程占用内存。类似虚存中,有的程序段被放到了硬盘上。

等待挂起状态:该状态是由等待状态切换,进程在外出中并等待某事件的出现就绪挂起状态:进程在外存中,但只要进入内存,即可运行

参考文献地址:

3.2内存管理

内存管理的主要功能:

地址转换:将程序中的逻辑地址转换成内存中的物理地址(抽象)存储保护:保证个个作业在自己的内存空间内运行,互不干扰(保护)内存的分配与回收:当作业或进程创建后系统会为他们分配内存空间,当结束后内存空间也会被回收。内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存(虚拟化)进程间通信(共享)

内存分配的两种方式:

连续分配方式

连续分配方式是指为一个进程分配一个连续的内存空间;连续分配方式主要包括单一连续分配、固定分区分配和动态分区分配。

单一连续分配

整个内存直接交由一个程序独占,当其他程序需要使用的时候,需要覆盖(Overlay)整个内存,即一次只能运行一个程序;优点:简单高效,无外部碎片。缺点:可能存在大量内部碎片,内存利用率低,且只能用于单用户。

固定分区分配

将用户内存空间划分为若干固定大小的区域,每个区域只装入一个进程。优点:程序可能太大放入不了任何一个分区。缺点:程序太小独占一个分区,造成内部碎片。

动态分区分配

在程序装入内存时,根据进程的大小动态地建立分区,并使得分区的大小正好适合进程的需要,因此系统中分区的大小和数目是可变的。优点:一开始内存利用率较高。缺点:随着时间推移,内存中会产生越来越多的小的内存块,内存的利用率也随之下降,这些碎片成为外部碎片。操作系统可以通过对进程进行碎片整理来解决碎片问题。

离散分配方式

由于连续分配方式会形成许多内存碎片,内存使用率极低;从而产生了离散分配方式,即将一个进程分散地装入到许多不相邻的内存分区中。把主存空间划分为大小相等且固定地块,块相对较小,作为主存地基本单位。每个进程以块为单位进行划分,进程在执行时,以块为单位逐个申请主存中地块空间。

分页存储(页式存储)

基本概念:

进程中的块:页或页面页或页面的编号:页号内存中的块:页框或页帧页框的编号:块号或页帧号页表:每个进程页号对应块号的映射表页表项:页号 + 块号逻辑地址:页号 + 页内偏移量物理地址:块号 + 块内偏移量块的大小=页的大小,所以块内偏移量=页内偏移量

逻辑地址转换为物理地址

根据页面大小(4K)可计算出页内地址的位数(4k=2的12次方,二进制位数是12位,等同于十六进制是3位)页内地址位数(3位十六进制)结合逻辑地址(5148H)计算出页内地址(148H)和页号(5H)页号结合页表,即可得出块号(假设页号5对应的块号7)块号(7H)+ 块内地址(等于页内地址,即148H)= 物理地址(7148H)

分段存储(段式存储)

逻辑空间分为若干个段,每个段定义了一组有完整逻辑意义的信息(如主程序段、子程序段、数据段等),段长度不等

基本概念

逻辑地址:(段号, 段内偏移量),(0,30)物理地址:(基址,段内地址),(40,70)

地址映射

根据段号从段表中找到段长和基址判断段内偏移量是否小于段长根据基址和段长计算出物理地址

分段与分页的区别

段页式存储 综合分段和分页存储方式,先按逻辑结构分段,再将每个段分页。地址结构

段页式存储

综合分段和分页存储方式,先按逻辑结构分段,再将每个段分页。

地址结构

地址映射

逻辑地址----- >(段号、段内页号、页内地址)段表寄存器--- >段表始址段号+段表始址---- >页表始址页表始址+段内页号----->存储块号块号+页内地址------>物理地址在被调进程的PCB中取出段表始址和段表长度,装入段表寄存器段号与控制寄存器的页表长度比较,若页号大于等于段表长度,发生地址越界中断,停止调用,否则继续由段号结合段表始址求出页表始址和页表大小页号与段表的页表大小比较,若页号大于等于页表大小,发生地址越界中断,停止调用,否则继续由页表始址结合段内页号求出存储块号存储块号&页内地址,即得物理地址

参考文献地址:

3.3网络协议栈

数据报文的封装与分用

封装:当应用程序用 TCP 协议传送数据时,数据首先进入内核网络协议栈中,然后逐一通过 TCP/IP 协议族的每层直到被当作一串比特流送入网络。对于每一层而言,对收到的数据都会封装相应的协议首部信息(有时还会增加尾部信息)。TCP 协议传给 IP 协议的数据单元称作 TCP 报文段,或简称 TCP 段(TCP segment)。IP 传给数据链路层的数据单元称作 IP 数据报(IP datagram),最后通过以太网传输的比特流称作帧(Frame)。

分用:当目的主机收到一个以太网数据帧时,数据就开始从内核网络协议栈中由底向上升,同时去掉各层协议加上的报文首部。每层协议都会检查报文首部中的协议标识,以确定接收数据的上层协议。这个过程称作分用。

Linux内核网络协议栈

协议栈的全景图

协议栈的分层结构

逻辑抽象层级:

物理层:主要提供各种连接的物理设备,如各种网卡,串口卡等。链路层:主要提供对物理层进行访问的各种接口卡的驱动程序,如网卡驱动等。网路层:是负责将网络数据包传输到正确的位置,最重要的网络层协议是 IP 协议,此外还有如 ICMP,ARP,RARP 等协议。传输层:为应用程序之间提供端到端连接,主要为 TCP 和 UDP 协议。应用层:顾名思义,主要由应用程序提供,用来对传输数据进行语义解释的 “人机交互界面层”,比如 HTTP,SMTP,FTP 等协议。

协议栈实现层级:

硬件层(Physical device hardware):又称驱动程序层,提供连接硬件设备的接口。

设备无关层(Device agnostic interface):又称设备接口层,提供与具体设备无关的驱动程序抽象接口。这一层的目的主要是为了统一不同的接口卡的驱动程序与网络协议层的接口,它将各种不同的驱动程序的功能统一抽象为几个特殊的动作,如 open,close,init 等,这一层可以屏蔽底层不同的驱动程序。

网络协议层(Network protocols):对应 IP layer 和 Transport layer。毫无疑问,这是整个内核网络协议栈的核心。这一层主要实现了各种网络协议,最主要的当然是 IP,ICMP,ARP,RARP,TCP,UDP 等。

协议无关层(Protocol agnostic interface),又称协议接口层,本质就是 SOCKET 层。这一层的目的是屏蔽网络协议层中诸多类型的网络协议(主要是 TCP 与 UDP 协议,当然也包括 RAW IP, SCTP 等等),以便提供简单而同一的接口给上面的系统调用层调用。简单的说,不管我们应用层使用什么协议,都要通过系统调用接口来建立一个 SOCKET,这个 SOCKET 其实是一个巨大的 sock 结构体,它和下面的网络协议层联系起来,屏蔽了不同的网络协议,通过系统调用接口只把数据部分呈献给应用层。

BSD(Berkeley Software Distribution)socket:BSD Socket 层,提供统一的 SOCKET 操作接口,与 socket 结构体关系紧密。

INET(指一切支持 IP 协议的网络) socket:INET socket 层,调用 IP 层协议的统一接口,与 sock 结构体关系紧密。

系统调用接口层(System call interface),实质是一个面向用户空间(User Space)应用程序的接口调用库,向用户空间应用程序提供使用网络服务的接口。

协议栈的数据结构

msghdr:描述了从应用层传递下来的消息格式,包含有用户空间地址,消息标记等重要信息。iovec:描述了用户空间地址的起始位置。file:描述文件属性的结构体,与文件描述符一一对应。file_operations:文件操作相关结构体,包括 read()、write()、open()、ioctl() 等。socket:向应用层提供的 BSD socket 操作结构体,协议无关,主要作用为应用层提供统一的 Socket 操作。sock:网络层 sock,定义与协议无关操作,是网络层的统一的结构,传输层在此基础上实现了 inet_sock。sock_common:最小网络层表示结构体。inet_sock:表示层结构体,在 sock 上做的扩展,用于在网络层之上表示 inet 协议族的的传输层公共结构体。udp_sock:传输层 UDP 协议专用 sock 结构,在传输层 inet_sock 上扩展。proto_ops:BSD socket 层到 inet_sock 层接口,主要用于操作 socket 结构。proto:inet_sock 层到传输层操作的统一接口,主要用于操作 sock 结构。net_proto_family:用于标识和注册协议族,常见的协议族有 IPv4、IPv6。softnet_data:内核为每个 CPU 都分配一个这样的 softnet_data 数据空间。每个 CPU 都有一个这样的队列,用于接收数据包。sk_buff:描述一个帧结构的属性,包含 socket、到达时间、到达设备、各层首部大小、下一站路由入口、帧长度、校验和等等。sk_buff_head:数据包队列结构。net_device:这个巨大的结构体描述一个网络设备的所有属性,数据等信息。inet_protosw:向 IP 层注册 socket 层的调用操作接口。inetsw_array:socket 层调用 IP 层操作接口都在这个数组中注册。sock_type:socket 类型。IPPROTO:传输层协议类型 ID。net_protocol:用于传输层协议向 IP 层注册收包的接口。packet_type:以太网数据帧的结构,包括了以太网帧类型、处理方法等。rtable:路由表结构,描述一个路由表的完整形态。rt_hash_bucket:路由表缓存。dst_entry:包的去向接口,描述了包的去留,下一跳等路由关键信息。napi_struct:NAPI 调度的结构。NAPI 是 Linux 上采用的一种提高网络处理效率的技术,它的核心概念就是不采用中断的方式读取数据,而代之以首先采用中断唤醒数据接收服务,然后采用 poll 的方法来轮询数据。NAPI 技术适用于高速率的短长度数据包的处理。

网络协议栈初始化流程

这需要从内核启动流程说起。当内核完成自解压过程后进入内核启动流程,这一过程先在 arch/mips/kernel/head.S 程序中,这个程序负责数据区(BBS)、中断描述表(IDT)、段描述表(GDT)、页表和寄存器的初始化,程序中定义了内核的入口函数 kernel_entry()、kernel_entry() 函数是体系结构相关的汇编代码,它首先初始化内核堆栈段为创建系统中的第一过程进行准备,接着用一段循环将内核映像的未初始化的数据段清零,最后跳到 start_kernel() 函数中初始化硬件相关的代码,完成 Linux Kernel 环境的建立。

start_kenrel() 定义在 init/main.c 中,真正的内核初始化过程就是从这里才开始。函数 start_kerenl() 将会调用一系列的初始化函数,如:平台初始化,内存初始化,陷阱初始化,中断初始化,进程调度初始化,缓冲区初始化,完成内核本身的各方面设置,目的是最终建立起基本完整的 Linux 内核环境。

start_kernel() 中主要函数及调用关系如下:

start_kernel() 的过程中会执行 socket_init() 来完成协议栈的初始化,实现如下:

void sock_init(void)//网络栈初始化{	int i; 	printk("Swansea University Computer Society NET3.019\n"); 	/*	 *	Initialize all address (protocol) families. 	 */	 	for (i = 0; i < NPROTO; ++i) pops[i] = NULL; 	/*	 *	Initialize the protocols module. 	 */ 	proto_init(); #ifdef CONFIG_NET	/* 	 *	Initialize the DEV module. 	 */ 	dev_init();  	/*	 *	And the bottom half handler 	 */ 	bh_base[NET_BH].routine= net_bh;	enable_bh(NET_BH);#endif  }
sock_init() 包含了内核协议栈的初始化工作:sock_init:Initialize sk_buff SLAB cache,注册 SOCKET 文件系统。net_inuse_init:为每个 CPU 分配缓存。proto_init:在 /proc/net 域下建立 protocols 文件,注册相关文件操作函数。net_dev_init:建立 netdevice 在 /proc/sys 相关的数据结构,并且开启网卡收发中断;为每个 CPU 初始化一个数据包接收队列(softnet_data),包接收的回调;注册本地回环操作,注册默认网络设备操作。inet_init:注册 INET 协议族的 SOCKET 创建方法,注册 TCP、UDP、ICMP、IGMP 接口基本的收包方法。为 IPv4 协议族创建 proc 文件。此函数为协议栈主要的注册函数:rc = proto_register(&udp_prot, 1);:注册 INET 层 UDP 协议,为其分配快速缓存。(void)sock_register(&inet_family_ops);:向 static const struct net_proto_family *net_families[NPROTO] 结构体注册 INET 协议族的操作集合(主要是 INET socket 的创建操作)。inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0;:向 externconst struct net_protocol *inet_protos[MAX_INET_PROTOS] 结构体注册传输层 UDP 的操作集合。static struct list_head inetsw[SOCK_MAX]; for (r = &inetsw[0]; r < &inetsw[SOCK_MAX];++r) INIT_LIST_HEAD(r);:初始化 SOCKET 类型数组,其中保存了这是个链表数组,每个元素是一个链表,连接使用同种 SOCKET 类型的协议和操作集合。for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q):inet_register_protosw(q);:向 sock 注册协议的的调用操作集合。arp_init();:启动 ARP 协议支持。ip_init();:启动 IP 协议支持。udp_init();:启动 UDP 协议支持。dev_add_pack(&ip_packet_type);:向 ptype_base[PTYPE_HASH_SIZE]; 注册 IP 协议的操作集合。socket.c 提供的系统调用接口。

协议栈初始化完成后再执行 dev_init(),继续设备的初始化。

Socket 创建流程

协议栈收包流程概述

硬件层与设备无关层:硬件监听物理介质,进行数据的接收,当接收的数据填满了缓冲区,硬件就会产生中断,中断产生后,系统会转向中断服务子程序。在中断服务子程序中,数据会从硬件的缓冲区复制到内核的空间缓冲区,并包装成一个数据结构(sk_buff),然后调用对驱动层的接口函数 netif_rx() 将数据包发送给设备无关层。该函数的实现在 net/inet/dev.c 中,采用了 bootom half 技术,该技术的原理是将中断处理程序人为的分为两部分,上半部分是实时性要求较高的任务,后半部分可以稍后完成,这样就可以节省中断程序的处理时间,整体提高了系统的性能。

NOTE:在整个协议栈实现中 dev.c 文件的作用重大,它衔接了其下的硬件层和其上的网络协议层,可以称它为链路层模块,或者设备无关层的实现。

网络协议层:就以 IP 数据报为例,从设备无关层向网络协议层传递时会调用 ip_rcv()。该函数会根据 IP 首部中使用的传输层协议来调用相应协议的处理函数。

UDP 对应 udp_rcv()、TCP 对应 tcp_rcv()、ICMP 对应 icmp_rcv()、IGMP 对应 igmp_rcv()。以 tcp_rcv() 为例,所有使用 TCP 协议的套接字对应的 sock 结构体都被挂入 tcp_prot 全局变量表示的 proto 结构之 sock_array 数组中,采用以本地端口号为索引的插入方式。

所以,当 tcp_rcv() 接收到一个数据包,在完成必要的检查和处理后,其将以 TCP 协议首部中目的端口号为索引,在 tcp_prot 对应的 sock 结构体之 sock_array 数组中得到正确的 sock 结构体队列,再辅之以其他条件遍历该队列进行对应 sock 结构体的查询,在得到匹配的 sock 结构体后,将数据包挂入该 sock 结构体中的缓存队列中(由 sock 结构体中的 receive_queue 字段指向),从而完成数据包的最终接收。

NOTE:虽然这里的 ICMP、IGMP 通常被划分为网络层协议,但是实际上他们都封装在 IP 协议里面,作为传输层对待。

协议无关层和系统调用接口层:当用户需要接收数据时,首先根据文件描述符 inode 得到 socket 结构体和 sock 结构体,然后从 sock 结构体中指向的队列 recieve_queue 中读取数据包,将数据包 copy 到用户空间缓冲区。数据就完整的从硬件中传输到用户空间。这样也完成了一次完整的从下到上的传输。

协议栈发包流程概述

1、应用层可以通过系统调用接口层或文件操作来调用内核函数,BSD socket 层的 sock_write() 会调用 INET socket 层的 inet_wirte()。INET socket 层会调用具体传输层协议的 write 函数,该函数是通过调用本层的 inet_send() 来实现的,inet_send() 的 UDP 协议对应的函数为 udp_write()。

2、在传输层 udp_write() 调用本层的 udp_sendto() 完成功能。udp_sendto() 完成 sk_buff 结构体相应的设置和报头的填写后会调用 udp_send() 来发送数据。而在 udp_send() 中,最后会调用 ip_queue_xmit() 将数据包下放的网络层。

3、在网络层,函数 ip_queue_xmit() 的功能是将数据包进行一系列复杂的操作,比如是检查数据包是否需要分片,是否是多播等一系列检查,最后调用 dev_queue_xmit() 发送数据。

4、在链路层中,函数调用会调用具体设备提供的发送函数来发送数据包,e.g. dev->hard_start_xmit(skb, dev);。具体设备的发送函数在协议栈初始化的时候已经设置了。这里以 8390 网卡为例来说明驱动层的工作原理,在 net/drivers/8390.c 中函数 ethdev_init() 的设置如下:

/* Initialize the rest of the 8390 device structure. */  int ethdev_init(struct device *dev)  {      if (ei_debug > 1)          printk(version);            if (dev->priv == NULL) { //申请私有空间          struct ei_device *ei_local; //8390 网卡设备的结构体                    dev->priv = kmalloc(sizeof(struct ei_device), GFP_KERNEL); //申请内核内存空间          memset(dev->priv, 0, sizeof(struct ei_device));          ei_local = (struct ei_device *)dev->priv;  #ifndef NO_PINGPONG          ei_local->pingpong = 1;  #endif      }            /* The open call may be overridden by the card-specific code. */      if (dev->open == NULL)          dev->open = &ei_open; // 设备的打开函数      /* We should have a dev->stop entry also. */      dev->hard_start_xmit = &ei_start_xmit; // 设备的发送函数,定义在 8390.c 中      dev->get_stats   = get_stats;  #ifdef HAVE_MULTICAST      dev->set_multicast_list = &set_multicast_list;  #endif        ether_setup(dev);                return 0;  }

UDP的收发包流程总览

内核中断收包流程

UDP 收包流程

UDP 发包流程

3.4设备驱动

设备驱动程序简介

系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件只是个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。

设备驱动与用户程序的区别:

1、应用程序以main 开始;驱动程序没有main,它以一个模块初始化函数作为入口。2、应用程序从头到尾执行一个任务;驱动程序完成初始化之后不再运行,等待系统调用。3、应用程序可以使用GLIBC 等标准C 函数库;驱动程序不能使用标准C库。4、设备驱动提供什么能力,应用程序提供策略即如何使用这些能力。

设备驱动程序是内核的一部分,它主要完成4个功能:

1、对设备初始化和释放;2、把数据从内核传送到硬件,从硬件读取数据;3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据;4、检测和处理设备出现的错误。

用户态和内核态:

1、系统运行时一般情况下,分用户态和内核态,这两种运行态下的数据互不可见的。2、驱动程序是内核的一部分,工作在内核态;应用程序工作在用户态。3、这样就存在数据空间访问的问题: 无法通过指针直接将二者的数据地址进行传递。

系统提供一系列函数帮助完成数据空间转移如:

get_user 、put_user 、copy_from_user 、copy_to_user等函数

关系图:

(1)、硬件、驱动、操作系统和应用程序的关系:

(2)、设备驱动与软硬件系统

(3)、应用程序与库函数、内核、驱动

设备的分类和特点

字符设备

(1)字符设备是面向数据流的设备,没有请求缓冲区,对设备的存取只能按顺序按字节的存取而不能随机访问。(2)Linux下的大多设备都是字符设备。应用程序是通过字符设备节点来访问字符设备的。通常至少需要实现 open, close, read, 和 write 等系统调用。(3)设备节点一般都由mknod命令都创建在/dev目录下,包含了设备的类型、主/次设备号以及设备的访问权限控制等,如:

   crw-rw----  1 root  root 4, 64 Feb 18 23:34 /dev/ttyS0

(4)常见的字符设备有鼠标、键盘、串口、控制台等。

当然,也有可以随机访问的字符设备,比如磁带驱动器,但访问随机数据所需要的时间很大程度上依赖于数据在设备内的位置。

块设备

(1)存储设备一般属于块设备,块设备有****请求缓冲区,并且支持随机访问而不必按照顺序去存取数据,比如你可以先存取后面的数据,然后在存取前面的数据,这对字符设备来说是不可能的。(2)尽管在Linux下有块设备节点,但应用程序一般是通过文件系统及其高速缓存来访问块设备的,而不是直接通过设备节点来读写块设备上的数据。(3)每个块设备在/dev/目录下都有一个对应的设备文件,即设备节点,它们包含了设备的类型、主/次设备号以及设备的访问权限控制等 ,如

brw-rw----  1 root  root  3, 1 Jul  5  2000 /dev/hda1

(4)块设备既可以作为普通的裸设备用来存放任意数据,也可以将块设备按某种文件系统类型的格式进行格式化,然后按照该文件系统类型的格式来读取块设备上的数据。

常见的块设备有各种硬盘、flash磁盘、RAM磁盘等。

网络设备

(1)不同于字符设备和块设备,它是面向报文的而不是面向流的,它不支持随机访问,也没有请求缓冲区。(2)在Linux里一个网络设备也可以叫做一个网络接口,它没有像字符设备和块设备一样的设备号,只有一个唯一的名字如eth0、 eth1等,这个名字也不需要与设备文件节点对应,应用程序是通过Socket而不是设备节点来访问网络设备,在系统里根本就不存在网络设备节点。

参考文献地址:

3.5文件系统

基本组成

文件系统是操作系统中负责管理持久数据的子系统,说简单点,就是负责把用户的文件存到磁盘硬件中,因为即使计算机断电了,磁盘里的数据并不会丢失,所以可以持久化的保存文件。

文件系统的基本数据单位是文件,它的目的是对磁盘上的文件进行组织管理,那组织的方式不同,就会形成不同的文件系统。

Linux 最经典的一句话是:「一切皆文件」,不仅普通的文件和目录,就连块设备、管道、socket 等,也都是统一交给文件系统管理的。

Linux 文件系统会为每个文件分配两个数据结构:索引节点(index node)和目录项(directory entry),它们主要用来记录文件的元信息和目录层次结构。

索引节点,也就是 inode,用来记录文件的元信息,比如 inode 编号、文件大小、访问权限、创建时间、修改时间、数据在磁盘的位置等等。索引节点是文件的唯一标识,它们之间一一对应,也同样都会被存储在硬盘中,所以索引节点同样占用磁盘空间。

目录项,也就是 dentry,用来记录文件的名字、索引节点指针以及与其他目录项的层级关联关系。多个目录项关联起来,就会形成目录结构,但它与索引节点不同的是,目录项是由内核维护的一个数据结构,不存放于磁盘,而是缓存在内存。

由于索引节点唯一标识一个文件,而目录项记录着文件的名,所以目录项和索引节点的关系是多对一,也就是说,一个文件可以有多个别字。比如,硬链接的实现就是多个目录项中的索引节点指向同一个文件。

注意,目录也是文件,也是用索引节点唯一标识,和普通文件不同的是,普通文件在磁盘里面保存的是文件数据,而目录文件在磁盘里面保存子目录或文件。

录项和目录是一个东西吗?

虽然名字很相近,但是它们不是一个东西,目录是个文件,持久化存储在磁盘,而目录项是内核一个数据结构,缓存在内存。

如果查询目录频繁从磁盘读,效率会很低,所以内核会把已经读过的目录用目录项这个数据结构缓存在内存,下次再次读到相同的目录时,只需从内存读就可以,大大提高了文件系统的效率。

注意,目录项这个数据结构不只是表示目录,也是可以表示文件的。

那文件数据是如何存储在磁盘的呢?

磁盘读写的最小单位是扇区,扇区的大小只有 512B 大小,很明显,如果每次读写都以这么小为单位,那这读写的效率会非常低。

所以,文件系统把多个扇区组成了一个逻辑块,每次读写的最小单位就是逻辑块(数据块),Linux 中的逻辑块大小为 4KB,也就是一次性读写 8 个扇区,这将大大提高了磁盘的读写的效率。

以上就是索引节点、目录项以及文件数据的关系,下面这个图就很好的展示了它们之间的关系:

索引节点是存储在硬盘上的数据,那么为了加速文件的访问,通常会把索引节点加载到内存中。

另外,磁盘进行格式化的时候,会被分成三个存储区域,分别是超级块、索引节点区和数据块区。

超级块,用来存储文件系统的详细信息,比如块个数、块大小、空闲块等等。

索引节点区,用来存储索引节点;

数据块区,用来存储文件或目录数据;

我们不可能把超级块和索引节点区全部加载到内存,这样内存肯定撑不住,所以只有当需要使用的时候,才将其加载进内存,它们加载进内存的时机是不同的:

超级块:当文件系统挂载时进入内存;

索引节点区:当文件被访问时进入内存;

虚拟文件系统

文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统(Virtual File System,VFS)。

VFS 定义了一组所有文件系统都支持的数据结构和标准接口,这样程序员不需要了解文件系统的工作原理,只需要了解 VFS 提供的统一接口即可。在 Linux 文件系统中,用户空间、系统调用、虚拟机文件系统、缓存、文件系统以及存储之间的关系如下图:

Linux 支持的文件系统也不少,根据存储位置的不同,可以把文件系统分为三类:

磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。

内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的 /proc 和 /sys 文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据数据。

网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。

文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录。

文件的物理结构

在操作系统的辅助之下,磁盘中的数据在计算机中都会呈现为易读的形式,并且我们不需要关心数据到底是如何存放在磁盘中,存放在磁盘的哪个地方等等问题,这些全部都是由操作系统完成的。

文件块

磁盘中的存储单元会被划分为一个个的“块”,也被称为扇区,扇区的大小一般都为 512byte。这说明即使一块数据不足 512byte,那么它也要占用 512byte 的磁盘空间。

而几乎所有的文件系统都会把文件分割成固定大小的块来存储,通常一个块的大小为 4K。如果磁盘中的扇区为 512byte,而文件系统的块大小为 4K,那么文件系统的存储单元就为 8 个扇区。这也是前面提到的一个问题,文件大小和占用空间之间有什么区别?文件大小是文件实际的大小,而占用空间则是因为即使它的实际大小没有达到那么大,但是这部分空间实际也被占用,其他文件数据无法使用这部分的空间。所以我们写入 1byte 的数据到文本中,但是它占用的空间也会是 4K。

这里要注意在 Windows 下的 NTFS 文件系统中,如果一开始文件数据小于 1K,那么则不会分配磁盘块来存储,而是存在一个文件表中。但是一旦文件数据大于 1K,那么不管以后文件的大小,都会分配以 4K 为单位的磁盘空间来存储。

文件分配方式

不同的文件系统为文件分配磁盘空间会有不同的方式,这些方式各自都有优点和缺点。

连续分配

连续分配要求每个文件在磁盘上占有一组连续的块,该分配方式较为简单。

通过上图可以看到,文件的逻辑块号的顺序是与物理块号相同的,这样就可以实现随机存取了,只要知道了第一个逻辑块的物理地址,那么就可以快速访问到其他逻辑块的物理地址。那么操作系统如何完成逻辑块与物理块之间的映射呢?实际上,文件都是存放在目录下的,而目录是一种有结构文件,所以在文件目录的记录中会存放目录下所有文件的信息,每一个文件或者目录都是一个记录。而这些信息就包括文件的起始块号和占有块号的数量。

那么操作系统如何完成逻辑块与物理块之间的映射呢?(逻辑块号,块内地址)-> (物理块号,块内地址),只需要知道逻辑块号对应的物理块号即可,块内地址不变。

用户访问一个文件的内容,操作系统通过文件的标识符找到目录项 FCB,物理块号 = 起始块号 + 逻辑块号。当然,还需要检查逻辑块号是否合法,是否超过长度等。因为可以根据逻辑块号直接算出物理块号,所以连续分配支持顺序访问和随机访问。

因为读/写文件是需要移动磁头的,如果访问两个相隔很远的磁盘块,移动磁头的时间就会变长。使用连续分配来作为文件的分配方式,会使文件的磁盘块相邻,所以文件的读/写速度最快。

连续空间存放的方式虽然读写效率高,但是有「磁盘空间碎片」和「文件长度不易扩展」的缺陷。

如下图,如果文件 B 被删除,磁盘上就留下一块空缺,这时,如果新来的文件小于其中的一个空缺,我们就可以将其放在相应空缺里。但如果该文件的大小大于所有的空缺,但却小于空缺大小之和,则虽然磁盘上有足够的空缺,但该文件还是不能存放。当然了,我们可以通过将现有文件进行挪动来腾出空间以容纳新的文件,但是这个在磁盘挪动文件是非常耗时,所以这种方式不太现实。

另外一个缺陷是文件长度扩展不方便,例如上图中的文件 A 要想扩大一下,需要更多的磁盘空间,唯一的办法就只能是挪动的方式,前面也说了,这种方式效率是非常低的。

那么有没有更好的方式来解决上面的问题呢?答案当然有,既然连续空间存放的方式不太行,那么我们就改变存放的方式,使用非连续空间存放方式来解决这些缺陷。

非连续空间存放方式

非连续空间存放方式分为「链表方式」和「索引方式」。

链式分配

链式分配采取离散分配的方式,可以为文件分配离散的磁盘块。它有两种分配方式:显示链接和隐式链接。

隐式链接

隐式链接是只目录项中只会记录文件所占磁盘块中的第一块的地址和最后一块磁盘块的地址,然后通过在每一个磁盘块中存放一个指向下一磁盘块的指针,从而可以根据指针找到下一块磁盘块。如果需要分配新的磁盘块,则使用最后一块磁盘块中的指针指向新的磁盘块,然后修改新的磁盘块为最后的磁盘块。

我们来思考一个问题,采用隐式链接如何将实现逻辑块号转换为物理块号呢?

用户给出需要访问的逻辑块号 i,操作系统需要找到所需访问文件的目录项 FCB。从目录项中可以知道文件的起始块号,然后将逻辑块号 0 的数据读入内存,由此知道 1 号逻辑块的物理块号,然后再读入 1 号逻辑块的数据进内存,此次类推,最终可以找到用户所需访问的逻辑块号 i。访问逻辑块号 i,总共需要 i + 1 次磁盘 I/O 操作。

得出结论:隐式链接分配只能顺序访问,不支持随机访问,查找效率低。

我们来思考另外一个问题,采用隐式链接是否方便文件拓展?

我们知道目录项中存有结束块号的物理地址,所以我们如果要拓展文件,只需要将新分配的磁盘块挂载到结束块号的后面即可,修改结束块号的指针指向新分配的磁盘块,然后修改目录项。

得出结论:隐式链接分配很方便文件拓展。所有空闲磁盘块都可以被利用到,无碎片问题,存储利用率高。

显式链接

显示链接是把用于链接各个物理块的指针显式地存放在一张表中,该表称为文件分配表(FAT,File Allocation Table)。

由于查找记录的过程是在内存中进行的,因而不仅显著地提高了检索速度,而且大大减少了访问磁盘的次数。但也正是整个表都存放在内存中的关系,它的主要的缺点是不适用于大磁盘。

比如,对于 200GB 的磁盘和 1KB 大小的块,这张表需要有 2 亿项,每一项对应于这 2 亿个磁盘块中的一个块,每项如果需要 4 个字节,那这张表要占用 800MB 内存,很显然 FAT 方案对于大磁盘而言不太合适。

索引分配

链表的方式解决了连续分配的磁盘碎片和文件动态扩展的问题,但是不能有效支持直接访问(FAT除外),索引的方式可以解决这个问题。

索引的实现是为每个文件创建一个「索引数据块」,里面存放的是指向文件数据块的指针列表,说白了就像书的目录一样,要找哪个章节的内容,看目录查就可以。

另外,文件头需要包含指向「索引数据块」的指针,这样就可以通过文件头知道索引数据块的位置,再通过索引数据块里的索引信息找到对应的数据块。

创建文件时,索引块的所有指针都设为空。当首次写入第 i 块时,先从空闲空间中取得一个块,再将其地址写到索引块的第 i 个条目。

索引的方式优点在于:

文件的创建、增大、缩小很方便;不会有碎片的问题;支持顺序读写和随机读写;

由于索引数据也是存放在磁盘块的,如果文件很小,明明只需一块就可以存放的下,但还是需要额外分配一块来存放索引数据,所以缺陷之一就是存储索引带来的开销。

如果文件很大,大到一个索引数据块放不下索引信息,这时又要如何处理大文件的存放呢?我们可以通过组合的方式,来处理大文件的存。

先来看看链表 + 索引的组合,这种组合称为「链式索引块」,它的实现方式是在索引数据块留出一个存放下一个索引数据块的指针,于是当一个索引数据块的索引信息用完了,就可以通过指针的方式,找到下一个索引数据块的信息。那这种方式也会出现前面提到的链表方式的问题,万一某个指针损坏了,后面的数据也就会无法读取了。

还有另外一种组合方式是索引 + 索引的方式,这种组合称为「多级索引块」,实现方式是通过一个索引块来存放多个索引数据块,一层套一层索引,像极了俄罗斯套娃是吧。

空闲空间的管理

前面说到的文件的存储是针对已经被占用的数据块组织和管理,接下来的问题是,如果我要保存一个数据块,我应该放在硬盘上的哪个位置呢?难道需要将所有的块扫描一遍,找个空的地方随便放吗?

那这种方式效率就太低了,所以针对磁盘的空闲空间也是要引入管理的机制,接下来介绍几种常见的方法:

空闲表法空闲链表法位图法空闲表法

空闲表法就是为所有空闲空间建立一张表,表内容包括空闲区的第一个块号和该空闲区的块个数,注意,这个方式是连续分配的。如下图:

当请求分配磁盘空间时,系统依次扫描空闲表里的内容,直到找到一个合适的空闲区域为止。当用户撤销一个文件时,系统回收文件空间。这时,也需顺序扫描空闲表,寻找一个空闲表条目并将释放空间的第一个物理块号及它占用的块数填到这个条目中。

这种方法仅当有少量的空闲区时才有较好的效果。因为,如果存储空间中有着大量的小的空闲区,则空闲表变得很大,这样查询效率会很低。另外,这种分配技术适用于建立连续文件。

文件系统的结构

文件的存储

前面提到 Linux 是用位图的方式管理空闲空间,用户在创建一个新文件时,Linux 内核会通过 inode 的位图找到空闲可用的 inode,并进行分配。要存储数据时,会通过块的位图找到空闲的块,并分配,但仔细计算一下还是有问题的。

数据块的位图是放在磁盘块里的,假设是放在一个块里,一个块 4K,每位表示一个数据块,共可以表示 4 * 1024 * 8 = 2^15 个空闲块,由于 1 个数据块是 4K 大小,那么最大可以表示的空间为 2^15 * 4 * 1024 = 2^27 个 byte,也就是 128M。

也就是说按照上面的结构,如果采用「一个块的位图 + 一系列的块」,外加「一个块的 inode 的位图 + 一系列的 inode 的结构」能表示的最大空间也就 128M,这太少了,现在很多文件都比这个大。

在 Linux 文件系统,把这个结构称为一个块组,那么有 N 多的块组,就能够表示 N 大的文件。

最终,整个文件系统格式就是下面这个样子。

最前面的第一个块是引导块,在系统启动时用于启用引导,接着后面就是一个一个连续的块组了,块组的内容如下:

超级块,包含的是文件系统的重要信息,比如 inode 总个数、块总个数、每个块组的 inode 个数、每个块组的块个数等等。

块组描述符,包含文件系统中各个块组的状态,比如块组中空闲块和 inode 的数目等,每个块组都包含了文件系统中「所有块组的组描述符信息」。

数据位图和 inode 位图, 用于表示对应的数据块或 inode 是空闲的,还是被使用中。

inode 列表,包含了块组中所有的 inode,inode 用于保存文件系统中与各个文件和目录相关的所有元数据。

数据块,包含文件的有用数据。

可以会发现每个块组里有很多重复的信息,比如超级块和块组描述符表,这两个都是全局信息,而且非常的重要,这么做是有两个原因:

如果系统崩溃破坏了超级块或块组描述符,有关文件系统结构和内容的所有信息都会丢失。如果有冗余的副本,该信息是可能恢复的。

通过使文件和管理数据尽可能接近,减少了磁头寻道和旋转,这可以提高文件系统的性能。

不过,Ext2 的后续版本采用了稀疏技术。该做法是,超级块和块组描述符表不再存储到文件系统的每个块组中,而是只写入到块组 0、块组 1 和其他 ID 可以表示为 3、 5、7 的幂的块组中。

目录的存储

在前面,我们知道了一个普通文件是如何存储的,但还有一个特殊的文件,经常用到的目录,它是如何保存的呢?

基于 Linux 一切皆文件的设计思想,目录其实也是个文件,你甚至可以通过 vim 打开它,它也有 inode,inode 里面也是指向一些块。

和普通文件不同的是,普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。

在目录文件的块中,最简单的保存格式就是列表,就是一项一项地将目录下的文件信息(如文件名、文件 inode、文件类型等)列在表里。

列表中每一项就代表该目录下的文件的文件名和对应的 inode,通过这个 inode,就可以找到真正的文件。

通常,第一项是「.」,表示当前目录,第二项是「..」,表示上一级目录,接下来就是一项一项的文件名和 inode。

如果一个目录有超级多的文件,我们要想在这个目录下找文件,按照列表一项一项的找,效率就不高了。

于是,保存目录的格式改成哈希表,对文件名进行哈希计算,把哈希值保存起来,如果我们要查找一个目录下面的文件名,可以通过名称取哈希。如果哈希能够匹配上,就说明这个文件的信息在相应的块里面。

Linux 系统的 ext 文件系统就是采用了哈希表,来保存目录的内容,这种方法的优点是查找非常迅速,插入和删除也较简单,不过需要一些预备措施来避免哈希冲突。

目录查询是通过在磁盘上反复搜索完成,需要不断地进行 I/O 操作,开销较大。所以,为了减少 I/O 操作,把当前使用的文件目录缓存在内存,以后要使用该文件时只要在内存中操作,从而降低了磁盘操作次数,提高了文件系统的访问速度。

参考文献地址:

3.6依赖体系结构的代码

虽然Linux很大程度上独立于运行的架构,但为了正常运行和实现更高的效率,一些元素必须考虑架构。的。/linux/arch子目录定义了内核源代码中依赖于架构的部分,其中包含了各种特定于架构的子目录(它们共同构成了BSP)。对于典型的桌面系统,使用x86目录。每个架构子目录包含许多其他子目录,每个子目录专注于内核的特定方面,如引导、内核、内存管理等。这些依赖于架构的代码可以在。/linux/arch。

如果Linux内核的可移植性和效率不够好的话,Linux还提供了一些其他的特性,不能归入以上几类。作为生产操作系统和开源软件,Linux是测试新协议及其增强的良好平台。

Linux支持大量的网络协议,包括典型的TCP/IP,以及高速网络的扩展(大于1千兆以太网[GbE]和10 GbE)。Linux还可以支持诸如流控制传输协议(SCTP)之类的协议,它提供了比TCP更高级的特性(它是传输层协议的继承者)。Linux也是一个动态内核,支持动态添加或删除软件组件。它们被称为可动态加载的内核模块,可以由用户根据需要在引导时插入(目前,一个特定的设备需要这个模块),也可以在任何时候插入。Linux的最新增强是可以作为其他操作系统使用的操作系统(称为hypervisor)。最近,内核被修改并被称为基于内核的虚拟机(KVM)。这一修改为用户空间启用了一个新的接口,允许其他操作系统在启用KVM的内核上运行。除了运行Linux的其他实例,Microsoft Windows也可以虚拟化。唯一的限制是底层处理器必须支持新的虚拟化指令。

3.7内核参考书籍《深入了解Linux内核》《Linux就该这么学》《Linux内核完全注释V3.0书签版》《Linux命令行大全 - 绍茨 (william E.shotts)》《Linux命令速查手册》《Linux性能优化大师》《Linux环境编程:从应用到内核》《Linux集群和自动化运维 余洪春》《Linux驱动程序开发实例(第2版)》《Linux高级程序设计(第3版)》《构建高可用Linux服务器(第4版)》四、Linux内核路线

很多同学对Linux接触很少,对Linux平台的开发一无所知。现在,趋势越来越表明,作为一个优秀的软件开发者或者计算机IT从业者,掌握Linux是一个非常重要的谋生资源和手段。接下来我将结合我个人几年的开发经验,谈谈Linux的学习方法和学习中应该注意的一些事情,特别是关于Linux,类UNIX系统和开源软件文化。

就像我刚才说的,很多同学之前可能连Linux是什么都不知道,更别说UNIX了。所以我们从最基础的一点开始,Linux和UNIX的历史我们就不多说了,直接进入入门学习。

Linux入门非常简单。问题是你有没有耐心,有没有爱折腾,有没有不排除重装之类的大修。可以说不折腾是学不好Linux的。鸟哥说你要真正了解Linux的分区机制,并且对LVM的使用相当熟练。不超过20次是无法积累Linux安装经验的,所以不要怕折腾。

既然之前大家都用Windows,我也尽量照顾这些“菜鸟”。我的推荐,如果你是第一次接触Linux,那就先在虚拟机里试试。我推荐虚拟机用的Virtual Box。我不提倡使用VM,因为VM是开源的,是收费的。我不想推广盗版。当然,如果你有足够的钱,你可以试试VM,但我想说的是,即使是VM也不一定好。付费软件不一定好。首先,虚拟盒子很小。Windows平台下安装包80MB左右,而VM每转600MB。虽然很强大,但是消耗了很多资源。更何况虚拟盒子完全可以满足你的需求。所以,还是自己选比较好。如何使用虚拟机是你的事。这个就不教你了,因为很简单。如果不能,可以用谷歌或者百度。如果你英语好,可以直接看官方文件。

现在介绍一下Linux发行的知识。如您所见,Linux发行版不是Linux。Linux仅指操作系统的内核。作为一个训练有素的学生,不要找我解释,我也没时间。

我推荐的发行版如下:

UBUNTU适合纯新手,追求稳定的官方支持,对系统稳定性要求弱,喜欢最新的应用,相对不喜欢折腾开发者。比UBUNTU难很多的发行版Debian,特点是稳定易用的包管理系统,缺点是缺乏企业支持,以社区开发为驱动。Arch,追逐时尚的开发者首选,优点是包更新相当快,升级无缝。基本上一次安装就可以一直工作,没有UBUNTU那样的版本概念。专业点叫滚动升级,让你的系统保持最新。缺点很明显,不稳定。同时安装配置也比Debian麻烦。比Arch更难的Gentoo,考验用户的综合水平。从系统安装到微调,内核编译都是手把手。是高手和黑客展示自己技术手段,按需配置符合自己要求的系统的首选。

Slackware与Gentoo类似:

社区维护的RedHat的副本CentOS,完全是用RedHat的源代码重新编译的,理论上和RedHat的兼容性是最好的。如果你专注于Linux服务器,比如网络管理和网站建设,那么CentOS就是你的选择。

LFS,终极黑客炫耀工具,完全从源代码安装编译系统。在安装之前,您只能获得一个文档。您所要做的就是按照文档中的说明,一步一步,一个订单一个订单地,一个一个地构建您的Linux包。完全在你的掌控之中,你想要什么就有什么。如果你制作了LFS,那就证明你的Linux技术相当不错。如果你能借鉴LFS文档,把Linux从源代码移植到嵌入式系统,我敢说你能在中国企业做得很好。

你得挑一个适合自己的系统,然后装在虚拟机里开始用。如果你想快速学习Linux,我有一个建议,你应该忘记图形界面。不要去想图形界面能不能为你的问题提供答案,而是去世界各地寻找,询问如何用命令行解决你的问题。在这个过程中,你最好掌握好Linux的命令,至少要知道常用的命令,同时要建立自己的知识库,里面包含了你积累的知识。

下一阶段需要学习Linux平台的C++/C++开发,以及Bash脚本编程,如果对Java有很深的兴趣,还需要学习Java。同样,我建议你抛弃图形界面的IDE,从VIM开始。为什么是VIM而不是Emacs?我无意挑起编辑器大战,但我认为VIM适合新手和手笨脑慢的开发者。Emacs的按键太多,太复杂,我很害怕。然后是GCC,Make,Eclip

se(Java,C++或者)。虽然Eclipse中列出了C++,但是我不建议用IDE开发C++,因为这不是Linux的文化,你很容易忽略一些应该注意的问题。IDE让你懒的跟猪一样懒。如果你对程序调试和测试感兴趣,你必须学好GDB。如果不是GDB,这也是一门必修课。

这是发展的第一步。注意,我没有提到任何关于Linux API的东西,现阶段也不关心这个。你要做的就是积累经验,Linux平台开发的经验。我推荐的书如下:《C语言编程》,或者谭浩强的。c,白皮书当然更好。++C++ Primer Plus是C推荐的,我不喜欢Java,所以不推荐。工具推荐VIM的官方手册,GCC中文文档,GDB中文文档,GNU开源软件开发指南(电子书),汇编语言编程(让你对库,链接,嵌入式汇编,编译器优化选项有个初步的了解,不深入)。

如果过不了这个阶段,就不用做了。这是底线,也是最基本的基础。否则,离开,不要开发Linux。不专业的Linux开发者做出来的程序与Linux文化或者UNIX文化相悖,程序走不了多远,也不可能像Bash、VIM这样神奇的产品。所以做不好就走人。

接下来进入Linux系统编程,唯一的选择,APUE,UNIX环境下的高级编程。反复读,10遍太少。如果你在大学能把这本书砸了,里面的内容都练过了,有作品,口语表达能力足够强,面试的时候就能说服所有考官。(可能有点夸张,但APUE绝对是圣经读物,连Windows程序员都从中汲取养分。谷歌创始人的案头书,扎伯克的床头读物。)

看完这本书,你会对Linux系统编程有很好的了解。Linux和Windows平台有什么区别?它们的优缺点是什么?我的总结如下:Windows平台开发难。微软的系统API一直在扩展。如果你想使用最新最高效的功能,你必须时刻学习最适合当前流行系统的功能。

不,Linux有大约100个核心API,所以你可以用很好的记忆力记住它们。而且会长期不变。为什么不呢?因为它兼容UNIX,符合POSIX标准。因此,Linux平台的开发大多集中在底层或服务器编程上。这是它的优势。当然图形是Linux的软肋,但从一个开发者的角度来说,我不在乎,因为我也能适应命令行。如果有更好的图形界面,我会把它作为礼物。另外,Windows是关闭的,你甚至不知道系统做了什么。你将永远被微软牵着鼻子走。

想想吧。如果微软说Win8不支持QQ,腾讯也不会哭死。而且Linux是完全开源的。如果不喜欢,可以自己改,只要足够熟练。另外,虽然Windows使用的人很多,但是使用的场合比较单一,以桌面为主。Linux各方面都有发展,尤其是云计算、服务器软件、嵌入式领域、企业应用,兼容性一流。由于POSIX可以在UNIX系统上无缝运行,因此Apple Mac和IBM AS400系列都完全支持它。

另外,Linux的开发环境支持绝对一流,无论是C/C++,Java,Bash,Python,PHP,Javascript,。。。。。。连C#都支持。而且微软除了Visual Stdio套件都不太友好吧?

如果你看了APUE后有很多感触,想验证你的一些想法或经验,推荐UNIX编程艺术,世界顶尖黑客将与你分享他们的观点。现在是时候转移注意力了。总的来说,我分为四个方向:网络、图形、嵌入式、设备驱动。

如果选择网络,细分的话,其他的不太熟悉,只说服务器软件编写和高性能并发程序编写。相对来说,这是网络编程中技术含量最高的,也是最底层的。需要很多经验,看很多书,做很多项目。

我的看法是以下面的顺序来看书:

APUE的深度阅读——尤其是进程、线程、IPC、套接字多核编程——Pthread一定要吃透,你是NBUNIX网络编程–第1卷,第2卷TCP/IP网络详解——是时候再看一遍以上两本书了。TCP/IP网络的详细说明–第2卷。我觉得看第二卷就差不多了。当然,最好还是看第三卷。尽力去看吧。Lighttpd源代码——这个服务器也很有名。NGX源代码——与Apache相比,Nginx的源代码更少。如果能大致看一下,就是NB了。看源码主要是学习里面的socket编程和并发控制,想想就激动。如果你有这些技能,你可以试试给暴雪发简历,给他们写服务器后台,以为全世界的魔兽都运行在你的服务器软件上。Linux内核TCP/IP协议栈——深入了解TCP/IP实现如果还是喜欢驱动设计,可以看看底层协议,比如链路层。给路由器,网卡,网络设备,嵌入式系统软件写驱动应该不是问题。当然,一般的网络公司,哪怕是百度级别的,都应该毫不犹豫的录用你。看后面的书只需要时间和经验,所以35岁之前就做吧!跳槽到给你未来的地方!

图形方向,我觉得图形方向也是很有前途的,以下几个方面:

Opengl的工业和游戏开发在国外已经比较成熟。

动画特效,比如皮克斯,在国外也比较成熟。

GPU计算技术可以应用于浏览器网页渲染和GPU计算资源利用。因为开源,所以有很多文档程序可以参考。如果能进入火狐开发,或者谷歌做浏览器开发,应该很不错。

嵌入式方向:嵌入式方向没说的,Linux很重要

掌握多种架构,不仅仅是X86,ARM,MCU等。必须理解。如果你不懂硬件,我预见你会死在路上,我也想往嵌入式方向走,但是我觉得就算是学电子的学生也比不过学校教嵌入式的方式。我劝你,做之前一定要了解硬件。如果你去做嵌入式应用开发,只能祝你好运了。不要碰上诺基亚、惠普这样的公司,否则你会很惨。

驱动设计:软件开发周期很长,硬件不一样,很快。每个月都有这么多新硬件诞生,如何让它们在Linux上工作是你的工作。因为Linux兼容性好,如果不是太低级的驱动,基本的C语言就可以了,系统架构影响不大。由于系统支持,您可能可以在ARM上使用PC硬件,但需要做一些更改。所以硬件驱动开发不像嵌入式,对硬件知识要求很高。可能的方向很多,比如家电,特别是像索尼、日立、希捷、富士康这样的工厂,比较稀缺。

五,高效学习Linux内核

学习linux内核不像学习语言。一个月或者三月就能掌握C或者java。学习linux内核需要循序渐进,掌握正确的linux内核学习路线非常重要。本文将分享一些学习linux内核的建议。

1. 了解操作系统的基本概念。如果没有,可以学习《操作系统:设计与实现》,Andrew S.Tanenbaum写的那本,以MINIX为例解释操作系统的概念。非常推荐。

2.有了操作系统的基本概念,你就可以理解Linux的机制了。推荐罗伯特·拉芙写的Linux内核设计与实现。这本书从概念上解释了Linux有什么以及它是如何工作的。这本书应该反复仔细阅读。

3.有了Linux内核的知识,我们还需要具体学习Linux内核源代码。经典的是丹尼尔·p·博韦特写的《深入理解Linux内核》。学习这本书的时候,要看看内核代码。这本书学起来挺费劲的,所以有很多代码要研究。但是,如果这本书很好理解,那么恭喜你,你已经对Linux内核很熟悉了。

4.如果你想开发设备驱动,可以向O 'Reilly Press学习Linux设备驱动。这本书是驾驶入门的好材料。还有一本很好的教材,精通Linux驱动开发,可以参考一下。开车,难免要学习一些硬件协议和资料。如果你研究的是哪一种,可以找相应的硬件文档,了解硬件的工作原理。这些我就不细说了。

5.网络部分,学习一些Linux网络部分学习《深入了解LINUX网络技术内幕》。这本书把Linux的网络部分讲得非常清楚透彻。不过我们一般不做这方面的研究,也不需要做那么多研究。毕竟现在相关岗位很少。

6.现在Linux相关的工作大多集中在一些嵌入式开发领域,如arm、mips等。你要学习以下关于架构的信息,了解CPU的设计和工作模式。看看ARM对应的芯片手册就知道了,很详细的。mips随便看看MIPS运行,有一两个版本。两个版本有些区别,建议全看。

7.补充一点经验。不要以为Linux庞大复杂,就很难学。认真学习,什么都可以学。就看你的毅力和恒心了。另外,不要走弯路,不要看市面上那些讲Linux0.11的书,学你想学的就好。就像学C语言看谭浩强一样,走弯路,费力气,严重影响学习效果。

关于linux内核学习路线,再多说几句应用编程,有时候经常会需要的:

1.学习Linux应用编程。建议看unix环境下的高级编程。把里面的例子都做一遍,你就系统的知道整个Linux编程了。

2.对于Linux,有一个Linux系统编程。学完上一个,这个一看就明白了。主要是针对Linux了解一些具体的内容,还是比较完整和实用的。

3.Linux网络编程,系统学习unix网络编程。第一卷,Socket联网api,基本上所有网络应用相关的程序都没问题。

这些内容,如果你打算用几年的时间一步一步的学习,就会成为Linux高手。建议个人参加零声教育的培训,学习效率会高很多。有目的的参与培训,缩短周期,快速成型是时代的需要。

以上就是Linux内核学习路线,关于学习Linux内核的建议,希望对小伙伴们有帮助。

标签: #游戏c语言后端框架是什么 #ethernet帧的发送过程模拟java #linux java调用c接口