操作系统 学习笔记

一、操作系统概述

1.1、Linux操作系统内核体系

在这里插入图片描述
操作系统可以比喻成一个软件外包公司,其内核就是这家公司的老板。

下面以“双击QQ”为入口,逐步进入操作系统的入门。

1、输入设备–接待人员

鼠标和键盘是计算机的输入设备,用户告诉计算机要做什么,都是通过输入设备。
输入设备驱动可以比喻成外包公司里的“客户对接员”,通过他来知道用户的需求。
当新插入一个鼠标时,会弹出一个通知,让你安装驱动,这就是这家外包公司给你配备对接人员呢。

2、输出设备–交付人员

显示器是输出设备,把计算机的处理结果呈现给客户。
显卡会有显卡驱动,在操作系统中称为输出设备驱动,可以比喻成"交付人员“。

3、中断事件

客户提需求时,客户希望外包公司把正在做的事情停下来,服务他,这时客户发送的需求被称为”中断事件“。当鼠标双击QQ时,会触发一个中断,这相当于客户告知对接员,我有新需求,需要你给处理下。你会事先把处理这种问题的方法教给客户端对接员,这在操作系统里就是调用中断处理函数。

4、程序-项目执行计划书

对于使用QQ,对外部公司来说,相当于接了个新业务,需要有一个项目执行计划书。内容就是,对于QQ程序来说,他能做哪些事,每件事怎样做,先做什么后做什么,把上面的逻辑都写到程序里,这个程序就相当于项目执行计划书。

5、文件管理系统

硬盘是个物理设备,要格式化成文件系统,才能存放程序。文件系统需要一个系统统一管理,这个系统成为文件管理系统。

6、程序&进程

QQ的二进制文件是静态的,成为程序。而运行起来的QQ,成为进程。

7、系统调用(System Call)

当多个进程想要用打印机来打印文件,他们不能打印到同一张纸上,否则第一行是A进程输出的内容,第二行是B进程输出的内容,这样就乱了。所以,打印机的行为是由操作系统内核来统一管理的,进程不能直接操作,这个由操作系统统一管理的就是系统调用。

系统调用会列出提供了哪些接口可以调用,然后进程在有需要的时候去调用。任何一个程序要运行起来,都需要通过系统调用来创建进程。

8、进程管理系统

在操作系统中,进程的执行也需要分配CPU来执行,所以,为了管理进程,我们还需要一个进程管理系统。如果运行的进程很多,则一个CPU会并发地运行多个进程,这就需要CPU的调度能力了。

9、内存管理系统

在操作系统中,不同的进程有不同的内存空间,这些内存空间需要统一的管理和分配,这就需要内存管理系统。

1.2、系统调用相关

1.2.1、Fork

在Linux里,要创建一个新的进程,需要一个老的进程调用fork来实现,这个老的进程叫做父进程,新的进程叫做子进程。

1.2.2、进程的内存空间

在操作系统里,每个进程都有自己的内存空间,不同进程之间互不干扰。

代码段(Code segment):对于进程空间,放程序代码的部分,称为代码段。

数据段(data segment):对于进程空间,存放进程运行中产生数据的部分,称为数据段。

堆(Heap):其中局部变量部分,在当前函数运行的时候起作用,当进入另一个函数时,这个变量就释放了;也有动态分配的,会较长时间保留,指明才销毁的,这部分内存空间称为“堆”。

1.2.3、brk和mmap

当内存分配数量较小时,用brk,会和原来的堆的数据连在一起。
当分配的内存较大时,使用mmap,会重新划分一块区域。

1.2.4、文件管理

Linux系统,一切皆文件:
1)启动一个进程,需要一个二进制文件。
2)启动进程时,需要加载一些配置文件如yml, properties等,这是文本文件
3)把日志打印到控制台上,是标准输出stdout文件
4)一个进程的输出作为另一个进程的输入,称为管道,管道也是一个文件
5)进程可以通过网络和其他进程通信,建立的socket,也是一个文件
6)进程需要访问的外部设备,也是一个文件
7)文件夹也是一个文件

每个文件,Linux都会分配一个文件描述符(File Descriptor),这是一个整数。有了这个文件描述符,我们就可以使用系统调用,查看或干预进程运行的方方面面。

1.2.5、信号处理 – 项目异常处理

通常进程中断,需要发送一个信号(signal),经常遇到的情况如下:
1)“Ctrl + C", 执行的命令终止退出
2)非法内存访问
3)硬件故障
4)用户进程通过kill函数,将信号发给另一个进程

1.2.6、进程间通信 – 项目组间沟通

1)消息队列:当两个进程需要交互的消息内容小的情况
2)共享内存:当两个进程需要交互的信息量比较大,用共享内存方式,可通过shmget创建一个共享内存块,通过shmat将共享内存映射到自己的内存空间,就可以读写了。

但是,当两个进程共同访问同一个内存中的数据时,就会存在竞争的问题。解决方式就是”排他“,通过信号量机制Semaphore。

Semaphore机制简介:对于只允许一个人访问的需求,可将信号量设为1。当一个人要访问时,先调用sem_wait,这时如果没有人在访问,则他就占用这个信号量,他就可以开始访问了;如果此时也有另一人要访问,也调用了sem_wait。这时后一人必须等待前一人访问完才能访问。当一人访问完成后,会调用seg_post将信号量释放,下一个等待的人就可以访问这个资源了。

1.2.7、网络通信-- 公司间沟通

不同机器间通过网络通信才能交互,交互时通过遵循相同的网络协议,如TCP/IP。
网络服务是通过Socket提供服务的,Socket可以理解为”插口“,在通信之前,双方要建立一个Socket。Socket也是一个文件,也有一个文件描述符,也可以通过读写函数进行通信。

1.2.8、Glibc – 中介

Glibc是Linux下使用开源的标准C库,Glibc为程序员提供丰富的API,除了如字符串处理、数学运算等用户态服务外,还封装了操作系统提供的系统服务,即系统调用的封装。

1.2.9、总结

在这里插入图片描述

二、Linux核心原理:系统初始化

2.1、x86架构

计算机硬件图:

在这里插入图片描述
CPU:对计算机来讲,CPU就是大脑,他是真正干活的,所有设备的执行都围绕他展开。

总线(Bus):CPU和其他设备连接,通过总线,其实就是主板上密密麻麻的集成电路,这些组成了CPU和其他设备的高速通道。

内存:在所有设备中,最重要的是内存(Memory),因为单靠CPU是无法完成计算任务的,很多复杂的计算需要把中间结果保持起来,然后基于中间结果做进一步的计算,所以CPU要依赖于内存。

总线上还有一些其他设备,例如显卡会连接显示器,磁盘控制器会连接硬盘,USB控制器会连接键盘和鼠标等。

CPU也不是单纯的一块,他包括三个部分:运算单元、数据单元、控制单元。
1)运算单元:只管计算,不管结果存哪里
2)数据单元:如果中间结果都存到数据里,那么每次计算都要经过总线去内存中拿,这太慢了,所以有了数据单元。数据单元包括CPU内部的缓存和寄存器组,空间很小,但速度飞快,可以暂时存放数据和运算结果。
3)控制单元:用来指挥到底做什么运算。他是一个统一的指挥中心,他可以获得下一条指令,然后执行这条指令。这个指令会知道运算单元取出数据单元中的某几个数据,计算出结果,然后放在数据单元的什么地方。
在这里插入图片描述
程序运行过程中要操作的数据和产生的计算结果,都会放在数据段里面。那CPU怎么执行这些程序,操作这些数据,产生一些结果,并写入到内存呢?

CPU的控制单元里面,有一个指令指针寄存器,他里面存放的是下一条指令在内存中的地址。控制单元会不停地将代码段指令拿进来,先放入指令寄存器。

当前的指令分两部分:一部分是做什么操作,如加法还是位移,一部分是操作哪些数据。

要执行这条指令,要把第一部分交个运算单元,第二部分交给数据单元。

数据单元根据数据的地址,从数据段里面读到数据寄存器里,就可以进行运算了。运算单元昨晚运算,产生的结果会暂存在数据单元的数据寄存器里。最终,会有指令将数据写回到内存中的数据段。

那么,怎样区分进程A和进程B呢?CPU里有两个寄存器,专门保持当前处理进程的代码段的起始地址,以及数据段的起始地址。这里面写的都是进程A,那当前执行的就是进程A的指令,等切换成了进程B,就会执行B的指令了,这个过程是进程切换。

CPU和内存直接传数据,靠的是总线。

总结:
在这里插入图片描述

2.2、从BIOS到bootloader – 创业伊始

在主板上,有一个东西叫ROM(Read Only Memory,只读存储器),和我们常说的内存RAM(Random Access Memory,随机存储器)不同。

RAM即平时买的内存条是可读可写的,而ROM是只读的,上面早就固化了一些初始化程序,也就是BIOS(Basic Input and Output System, 基本输入输出系统)。

在x86系统中,如果你有1M的内存空间:
在这里插入图片描述
将1M空间最上面的0xF0000到0xFFFFF这64K映射给ROM。

2.3、内核初始化 – 生意做大了,成立公司

2.3.1、0号进程

第一步:创建0号进程

在操作系统里面,先要有个创始进程,有一行指令 set_task_stack_end_magic(&init_task), 这里面有一个参数init_task,他的定义是struct task_struct init_task = INIT_TASK(init_task) 。他是系统创建的第一个进程,称为0号进程。这是唯一一个没有通过fork或kernel_thread产生的进程,是进程列表的第一个。

进程列表:可以比喻成公司接的所有的项目。

2.3.2、初始化办事大厅

第二步:初始化办事大厅,这样才能响应用户的需求。

这里对应的函数是trap_init(), 里面设置了很多中断门(Interrupt Gate),用于处理各种中断。

2.3.3、内存管理初始化 – 会议室

第三步:建立会议室,对应的,mm_init()就是用来初始化内存管理模块的。

第四步:项目需要项目管理进行调度,需要执行一定的调度策略。sched_init()就是用于初始化调度模块。

2.3.4、初始化1号进程

rest_init的第一个工作是,用kernel_thread(kernel_init, NULL, CLONE_FS)创建第二个进程,这个是1号进程。

1号进程对操作系统来讲,具有划时代意义。因为他将允许一个用户进程,这意味着这个公司老板把独立完成的任务,变成了交付该他人完成的制度。这个1号进程就相当于老板的大徒弟,有了第一个,就有第二个。

x86提供了分层的权限机制,把区域分层四个环(ring),越往里权限越高。
在这里插入图片描述
操作系统很好地利用了这个机制,把能够访问关键资源的代码放在ring0, 我们成为内核态(Kernel Mode);将普通的程序代码放在Ring3, 我们成为用户态(User Mode)。这样当用户态的代码想要执行更高的权限的指令,其行为将被禁止,以防止他们为所欲为。

如果用户态要访问核心资源,他就用通过系统调用这个办事大厅,进行请求,办事大厅后面就是内核态,用户态代码不用管后面发生了什么,只要拿到返回结果就可以了。

但一个用户态的程序运行到一半,要访问一个核心资源,例如要访问网卡发一个网络请求,就需要暂停当前的运行,调用系统调用,接下来就轮到内核中的代码运行了。

这个暂停怎么办呢?其实就是把程序运行到一半的情况保持下来。用内存来保持运行的中间结果,同时保存当前程序执行到那一行了,当前的栈在哪里,这些都是在寄存器里面的。

这个过程如下:
用户态 --> 系统调用–> 保持寄存器 -->内核态执行系统调用 --> 恢复寄存器 --> 返回用户态,然后接着运行。
在这里插入图片描述

2.3.5、创建2号进程

内核态的所偶遇进程由2号进程来统一管理,这是系统的第三个进程。

从用户态来看,创建进程可以比喻为公司启动一个项目,项目中包含有很多资源如会议室、资料库等。这个项目需要多个人去执行,多个人并行执行不同的部分,这就是多线程。如果只有一个人,他就是这个项目的主线程。

从内核态来看,无论是进程,还是线程,我们都称为任务(task),都使用相同的数据结构,放在同一个链表中。

三、Linux核心原理:进程管理

3.1、创建进程

1)先写代码,用系统调用创建进程
2)编译成二进制
3)运行的程序为进程
4)进程树:所有的进程都是由父进程fork处理的,所有进程的祖宗,就是系统启动的init进程
在这里插入图片描述
在解析Linux启动时,1号进程是 /sbin/init。

3.2、线程:让复杂的项目并行执行

进程可以比喻成公司接了一个项目,而线程可比喻成为了完成项目需求,而建立的一个个开发任务。

3.3、进程数据结构

有的进程只有一个线程,而有的进程有多个线程,他们都需要内核分配CPU来干活,那么怎样统一管理调度呢?
在Linux里面,无论线程还是进程,都叫做任务(task),由一个统一的结构task_struct来管理。
在这里插入图片描述
接下来看Linux的任务管理都应该干什么

首先,Linux先用一个链表,把所有task_struct存起来。
每一个任务都有一个ID,作为唯一标识。pid是process id, tgid是thread group id。

3.4、调度

对操作系统来讲,他的CPU梳理是有限的,干活全靠CPU,但进程数远大于CPU数,这就需要做进程的调度,有效地分配CPU时间,既要保证进程的最快响应,也要保证进程之间的公平,这是一个非常复杂的工作。

在Linux里,进程分两种:
1)实时进程:需要尽快执行返回结果
2)普通进程:大部分进程都是这种,按正常流程完成就可以,优先级没那么高。

调度时也有优先级,也有公平与非公平算法

四、内存管理

和会议室一样,内存被分层一块一块的,都编了号。例如3F-10,比如会议室3F-09,内存也有这样的地址,通过这个地址我们就能定位到物理内存的位置。

在程序里面,指令写入的地址是虚拟地址。例如,位置为10M的内存区域,操作系统会提供一种机制,将不通进程的虚拟地址和不同内存的物理地址映射起来。

当程序要访问虚拟地址的时候,由内核的数据结构进行转换,转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了。

内存管理系统会做三件事:
1、虚拟内存空间的管理,将虚拟内存分成大型相等的页
2、物理内存的管理,将物理内存分成大小相等的页
3、内存映射,将虚拟内存页和物理内存页映射起来,并且在内存紧张时,可以换出到硬盘中。

在这里插入图片描述

五、文件系统

规划文件系统要考虑下面几点:
1、文件系统要严格的组织形式:文件能以块为单位进行存储
2、文件系统中要有索引,用来方便查找一个文件的多个块都放在哪
在这里插入图片描述
3、如果文件系统中有的文件是热点文件,近期常被读取和写入,文件系统应该有缓存
4、文件应该用文件夹形式组织起来,方便管理和查询。

文件系统是一个树形目录:
在这里插入图片描述
5、Linux内核要在自己的内存里维护一套数据结构,来保持哪些文件被哪些进程打开和使用。

文件系统总结:
在这里插入图片描述

5.1、硬盘文件系统

目前Linux下最主流的文件系统格式:ext系列

类似于图书馆书架上大小相同的格子,硬盘也被分为相同大小的单元,称为块(block)。一个块的大小是扇区大小的整数倍,默认4K。在格式化时,这个值是可以设置的。

一大块硬盘被分成一个个小的block, 用来存放文件的数据,这样就不用给他分配一块连续的空间了。可以分散成一个个小块进行存放。

但这样带来的问题是,一个文件的数据存的太分散,查找起来不方便,这时,为了解决这个问题,就引入了元数据信息。元数据信息用inode来存储,inode中的 i 就是index, 每个文件会对应一个inode;一个文件夹也是一个文件,也对应一个inode。

inode中存放文件名、权限、属于哪个用户哪个组,大小多少,占多少个块。我们执行ls命令时,列出的权限、用户、大小信息,就是从inode中取出的。

硬链接和软链接:所谓的链接(Link),可以认为是文件的别名,链接分硬链接和软链接。
硬链接和原始文件共用一个inode,但inode是不跨文件系统的,每个文件系统都有自己的inode列表。
而软链接相当于重新创建一个文件,这个文件也有独立的inode,只不过打开这个文件看里面内容的时候,内容指向了另一个文件,软链接可以跨文件系统,甚至目标文件被删除了,链接文件还是存在的,只不过指向的文件找不到而已。

5.2、文件缓存

缓存就是内存中的一块空间,因为内存比硬盘快得多。Linux为了提升性能,有时会不直接操作硬盘,读写都在内存中。

因此,文件的IO分两种类型:

1、缓存IO:大多数文件系统默认IO都是缓存IO。对于读,先看内存中有没有,有则查内存后返回;没有就去读磁盘,然后存储在缓存中。对于写,先把数据从用户空间复制到内核空间,这时对于写的用户而言,写操作就已经完成了。至于什么时候写磁盘,由操作系统确定,触发显式地调用了sync同步命令。

2、直接IO:应用程序直接访问磁盘数据,不经过内核缓冲区,从而减少了内核缓存和用户程序之间的数据复制。

六、输入输出系统

在计算机系统中,CPU并不直接和输入输出设备大角度,而是通过一个”设备控制器(Device Control Unit)的组件,例如硬盘有硬盘控制器,USB有USB控制器,显示器有视频控制器等,他们就好像是代理一样。

输入输出设备分两类:块设备(Block Device)和字符设备(Character Device)。
块设备:将信息存储在大小相同的块中,每个块都有自己的地址。硬盘就是常见的块设备。
字符设备:字符设备发送或接收的是字节流。而不用考虑任何块结构,没有办法寻址。鼠标就是常用的字符设备。

由于块设备传输的数据量比较大,CPU先写入缓冲区,当缓冲区中的数据攒够一定数量,才会发送给设备;CPU读取的数据,也先读入到缓冲区,也是当缓冲区的数据攒够到一定数量,才会copy到内存。

CPU如何跟控制器的寄存器和数据缓冲区进行通信呢?
1)每个控制寄存器被分配一个IO端口,我们可以通过特殊的汇编指令操作这些寄存器
2)数据缓冲区,可内存映射IO,可以分配一段内存空间给他,就行读写内存一样读写数据缓冲区

对CPU来说,外部设备都会自行处理一些问题,例如,CPU给设备发送一条指令,让他读取一些数据,读完后,外部设备怎样通知CPU呢?控制器的寄存器一般会有状态标志位,可以通过检测状态的标志位,来确定输入或者输出操作社否完成。第一种方式就是轮训查询,一直查,直到查到完成。第二种方式是中断的方式,通知操作系统输入输出操作已经完成。
为了响应中断,会有一个硬件的中断控制器,当设备完成任务后出发中断到中断控制器,中断控制器就会通知CPU,一个中断产生了,CPU需要停下当前手里的活去处理中断。
在这里插入图片描述
有的设备需要读写大量数据,如果所有过程都让CPU协调的话,会占用CPU大量时间。例如,磁盘,需要支持DMA(Direct Memory Access)功能,即允许设备在CPU不参与的情况下,能够自行完成对内存的读写。实现DMA机制需要有个DMA控制器帮CPU来做协调:
在这里插入图片描述
CPU只需要对DMA控制器下指令,告知他想读多少数据,放在内存的某个地方就可以,接下来DMA控制器会发指令给磁盘控制器,读取磁盘上的数据到指定的内存位置,完成后,DMA控制器发中断通知CPU指令完成,CPU就可以直接用内存里的数据了。

用驱动程序屏蔽设备控制器的差异

由于每种设备的控制器的寄存器、缓冲区等使用模式都不同,对操作系统来说,需要个东西来屏蔽掉这些差异。这就是用来对接各个设备控制器的设备驱动程序。

设备控制器不属于操作系统的一部分,但设备驱动程序属于操作系统。操作系统的内核代码可以向调用本地代码一样调用设备驱动程序的代码,而驱动程序的代码需要发出特殊的面向设备控制器的指令,才能操作设备控制器。

七、进程间通信

如果把进程比喻成公司的项目组,那么对于大项目,就需要多个项目组合作完成,项目组之间怎样相互通信呢?

7.1、管道

拿Linux命令举例:

ps -ef | grep 关键字 | awk '{print $2}' | xargs kill -9

这里的竖线就是管道,他会将一个命令输出,作为后一个命令的输入。管道是一种单向数据传输机制,他其实就是一段缓存,里面的数据只能从一端写入,从另一端读出。
”|“的管道称为匿名管道,另一种类型是命名管道。通过mkfifo命令显式的创建:

mkfifo hello

hello就是这个管道的名称,管道以文件的形式存在,这也符合Linux一切皆文件的原则。

ls -l 

结果:
prw-r–r-- 1 pengshi staff 0 12 16 11:50 hello
其中的p,就是指管道。

接下来,可以向管道里写数据,如:

echo "hello world" > hello

这时,管道里的内容没有被读出,这个命令就hang在这里,这说明需要一个输出.
这时,我们在开一个控制台:

cat < hello

显示结果:hello world
刚才hang住的也完成了。

上面就是管道的方式实现的进程间的通信,可以看出,管道方式不适合进程间频繁的交换数据。所以引入了消息队列来实现进程间通信。

7.2、消息队列

消息队列不同于管道,管道是把发送的数据一次性发送,而消息队列把要发送的数据分层一个个固定大小的存储块(消息结构体),这在字节流上不连续。

使用消息队列,先定义消息结构体:

struct msg_buffer { 
	long mtype; 
	char mtext[1024];
};

再使用msgget函数来创建一个消息队列,函数有一个参数key, 是消息队列的唯一标识。

具体C怎样实现消息队列,此处省略

7.3、共享内存模型

如果两个进程要交互的数据太多,可以采用共享数据模型。
但不同的进程使用的内存是隔离的,怎样实现共享内存呢?可以拿出一块虚拟地址空间来,映射到相同的内存中,这样这个进程写入的东西,另一个进程马上就能看到了,就不需要做进程间数据的copy了。

通过shmget来创建共享内存。

7.3.1、信号量

对于共享内存,两个进程同时写,怎样不互相覆盖呢?这里需要一种类似”锁“的机制,保证同一个共享资源,同一时刻只能被一个进程访问,这就是信号量(Semaphore),信号量和共享内存往往配合使用。

信号量其实是一个计数器,主要用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

我们可以把信号量初始化一个数值,来代表某种资源的总数量。然后信号量有两种原子操作,一个是申请资源操作(P操作),会将信号量的数值减去n, 表示这n个数量已被申请了,其他人不能用了;另一个是资源归还操作(V操作),表示这些资源已经归还给信号量了,其他人可以用了。

可以通过semget函数来创建一个信号量。

7.4、信号

信号是异常情况下的工作模式。Linux提供了几十种信号,分别代表不同的意义。这就行警匪片里,对于紧急行动,”1号作战任务“开始执行,就执行…来处理紧急事件。

信号可以在任何时刻发送给某一进程,进程需要为这个信号配置信号处理函数。当某个信号发生的时候,就默认执行这个函数就行了。

可以通过kill -l查看所有的信号:
在这里插入图片描述
这些信号的作用,可以通过man 7 signal命令查看:

Signal     Value     Action   Comment
──────────────────────────────────────────────────────────────────────
SIGHUP        1       Term    Hangup detected on controlling terminal
                              or death of controlling process
SIGINT        2       Term    Interrupt from keyboard
SIGQUIT       3       Core    Quit from keyboard
SIGILL        4       Core    Illegal Instruction


SIGABRT       6       Core    Abort signal from abort(3)
SIGFPE        8       Core    Floating point exception
SIGKILL       9       Term    Kill signal
SIGSEGV      11       Core    Invalid memory reference
SIGPIPE      13       Term    Broken pipe: write to pipe with no
                              readers
SIGALRM      14       Term    Timer signal from alarm(2)
SIGTERM      15       Term    Termination signal
SIGUSR1   30,10,16    Term    User-defined signal 1
SIGUSR2   31,12,17    Term    User-defined signal 2
……

就像应急预案里给出的意义,每个信号都有一个唯一的ID,还有遇到这个信号的时候的默认操作。

7.4.1、信号的发送

Ctrl + C 产生 SIGINT信号, Ctrl + Z产生SIGTSTP信号。
有时候,硬件异常也产生信号。

八、网络系统

8.1、网络协议

一台机器将自己要表达的内容,按照某种约定好的格式发出去,另一个机器收到这些信息后,也能按照约定好的格式解析出来,从而准确可靠地获得发送方想要表达的内容,这种约定好的格式就是网络协议。

8.2、网络为什么要分层

假设我们有3台机器要同学,机器A和B不在统一网段,通过中间的一台Linux服务器进行转发:
在这里插入图片描述
网络协议模型分为2种:一是OSI的标准七层模型,一种是业界标准TCP/IP模型,他们对应关系如下:
在这里插入图片描述
为什么网络要分层呢?全球数以亿计的服务器和设备之间各有各的体系,但都可以通过一套网络协议栈,通过切分成多个层次和组合,来满足不同服务器和设备的通信需求。

第三层:网络层(IP层)

对网络协议的理解,先从第三层网络层入手,这里有我们熟悉的IP地址。
IP地址通常是这样子的:192.168.1.100/24。斜杠前面的是IP地址,IP地址被 ” . " 分成4个部分,每部分8位组成一个整数,8 * 4 = 32位,斜线后面的24表示,前24位(即前三个整数)表示网络号,后8位(即最后一个整数)表示主机号。
IP地址是互联网上的唯一地址。假设你要访问美国的一个IP地址,你可以从身边的网络出发,通过不断打听道儿,经过多个网络,最终到达。这个打听道儿的协议也在第三层,称为路由协议(Routing Protocol),将网络报从一个网络转发给另一个网络的设备称为路由器。

总而言之,第三层干的事,就是网络包从一个起始IP开始,沿着路由协议指的道儿,经过多个网络,经过多个路由转发,最终到达目标IP地址。

第二层:数据链路层(MAC层)

所谓MAC,就是每个网卡都有唯一的硬件地址(不绝对唯一,但大概率唯一,类似UUID),但这个地址没有全局定位功能。

MAC地址的定位功能局限在一个网络里,及同一个网络号下的IP地址,可通过MAC进行定位。通过IP地址获取MAC地址要通过ARP协议。

所以第二层干的事情,就是网络包在本地网络中服务器之间定位及通信的机制。

第一层:物理层

第一层就是物理设备,例如连着电脑的网线。

第四层:传输层(TCP层)

传输层有两个著名的协议:TCP和UDP。在第三层IP层的逻辑重,只负责数据从一个IP地址发送到另一个IP地址,至于丢包、乱序、重复、拥塞等,这些IP层都不管,所以处理这些逻辑的代码都放在了传输层的TCP协议里面。

我们知道,TCP是可靠的传输协议。因为第一层到第三层都不可靠,网络包说丢就丢,而TCP通过各种编号、重传等机制,让本来不可靠的网络对于上层来讲,变得“看起来”可靠。

第五层:应用层

我们常用的http, java写的servlet,都是应用层。

从第二层到第四层,都是在Linux内核里面处理的,而应用层例如浏览器、Nginx、Tomcat等都是用户态的。内核对于网络包的处理是不区分应用的。

从第四层再网上,需要区分从网络包发给哪个应用。在传输层的TCP或UDP协议里,都有端口的概念,不同应用监听不同的端口。例如nginx监听80端口,tomcat监听8080端口;再比如浏览器监听一个随机端口,FTP客户端监听另一个随机端口。

应用层和Linux内核之间的通信,是通过socket系统调用。Socket本身不属于任何一层,他只是操作系统的概念,而不是网络协议分层的概念。操作系统选择对于网络协议的实现方式是,2~4层在操作系统内核里面,而应用层的处理让应用自己去做,二者需要跨内核态和用户态通信,这就需要一个系统调用来完成这个衔接,这就是socket。

8.3、发送数据包

网络分完层后,对于数据包的发送,就是层层封装的过程。
在这里插入图片描述
例如,在Linux服务器B上部署了nginx和tomcat, 都是通过socket来监听80和8080端口。这样,操作系统内核就知道了,凡是发送给这2个端口的,就发送给这2个进程。

例如,在Linux服务器A上的客户端,打开一个浏览器连接nginx,也是通过socket, 其浏览器会被分配一个随机端口如12345。在浏览器,会将请求封装为http协议,通过socket发送给目标服务器的操作系统内核。在内核的网络协议栈里面,在TCP层创建了用于维护连接、序列号、重传、拥塞控制的数据结构:

  • 将http包上加上TCP头,发送给IP层
  • IP层加上IP头,发送给MAC层
  • MAC层加上MAC头,从硬件网卡发送出去。

网络包会先到达网络1(蓝色)的交换机,我们通常称交换机为二层设备,这是因为,交换机只会处理到第二层,然后他讲网络包的MAC头拿下来,发现目标MAC在自己右面的网口,于是就从这个网口发出去。

网络到会到达中金的Linux路由器,他左边的网口会收到网络包,发现MAC地址匹配,就交给IP层,在IP层通过IP头中的信息,在路由表中查找,下一跳在哪里,应该从哪个网口发出去。我们通常把路由器称为三层设备,因为他只会处理到第三层。

从路由器右边的网口发出去的包,会到网络2的交换机,还是会经历一次二层的处理,转发到交换机右面的网口。

最终的网络包会被转发到Linux服务器B,他发现MAC地址匹配,就将MAC头取下来,交给上一层。IP层发现IP地址匹配,将IP头取下来,交给上一层,TCP层会根据TCP头中的序列号等信息,发现他是一个正确的网络包,将网络包缓存起来,等待应用层读取。

应用层通过socket监听某个端口,因而读取的时候,操作系统内核会根据TCP头中的端口号,将网络包发给相应的应用。

http层的头和正文,是应用层来解析的。通过解析,应用层知道了客户端的请求,例如购买一个商品,还是请求一个网页,当应用层处理完http请求,会将结果仍然封装为http网络包,通过socket端口,发送给内核。

内核会经过层层封装,从物理网口发送出去,经过网络2的交换机,Linux路由器到达网络1,经过网络1的交换机,到达服务器A,在Linux服务器A上,经过层层解封,通过socket接口,根据客户端的随机端口号,发给客户端应用程序浏览器,于是,浏览器就完成了显式了。

这就是一次浏览器跨网络请求服务器,再接收返回结果做显示的过程。

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值