使用U盘传数据时操作系统做了什么(源码分析)

一、背景

学习linux文件系统时考虑一个具体的问题:我们经常会用U盘传输东西到计算机中。当我们把U盘插入到一台计算机上,并且从U盘中复制文件到计算机里,然后卸载U盘,将U盘拔出。操作系统在这一连串过程中做了些什么?这篇博客仅从文件系统的角度利用linux内核源码浅析该过程。本篇博客使用的linux内核版本为3.10.0。

二、文件系统基础

1.文件系统

在开始之前我想先简单介绍一下文件系统的基础知识。
我们都知道,计算机磁盘被抽象成了一个个块设备,一个块作为一个存储单元。我的系统块大小为1KB,linux中可以使用df命令来查看块大小。
在块抽象上还有另一层抽象,主要由三部分组成:超级块、i节点表、数据块。每个部分都由若干的块来组成。当然,数据块占用最多的块。
这里写图片描述
图中展示了这三个部分。其中引导块我不在这里详述。i节点也叫i-node,也可以叫做索引节点。叫法不同,实际指同一对象。
以EXT4文件系统为例,接下来我对这三个区域分别进行介绍:

超级块

超级块用来描述整个文件系统本身的信息。每个具体的文件系统如:EXT2、EXT3、EXT4、NTFS,它们都拥有自己的超级块。各自的超级块描述着各自文件系统的情况。
这里给大家展示EXT4文件系统的超级块在linux内核中的定义(部分):
这里写图片描述
EXT4文件系统的超级块定义存放在/fs/ext4文件夹下的ext4.h中

i节点

i节点存储于i节点表中。i节点中存储着大量的关于文件的重要信息,如文件的各种属性,文件的数据存储的位置等。
EXT4文件系统i节点在内核中的定义(部分):
这里写图片描述
EXT4文件系统的i节点定义存放在/fs/ext4文件夹下的ext4.h中

数据区

数据区,顾名思义,存储的是文件的具体数据。每个文件占用多个块来进行存储。通过i节点来指向这些数据块的位置。
当一个文件“静静”的在磁盘上时,i节点用来描述这个文件,每个文件对应一个i节点号,这个i节点号之于文件就相当于身份证号之于我们公民。i节点中存储着关于文件的各种属性,并且i节点也描述了文件数据的存储位置。
那么,现在有个问题。
当我们打开一个文件,需要读取文件的属性或者文件的数据时操作系统是怎么做的?当然这个文件的信息需要被读取到内存中进行处理。首先,我们需要知道我们有没有权限打开这个文件,那么肯定就要查看该文件i节点中存储的属性信息。因为文件权限就是文件的一个属性。所以该文件的i节点的信息需要被读取到内存中。
各种不同的文件系统的文件都可能被操作系统打开,所以i节点信息都可能需要读取到内存中。面对不同的文件系统,操作系统如何进行统一的管理就成了问题。由此,操作系统引入了一种抽象的文件系统,这种抽象的文件系统可以用同一组系统调用对各种不同的文件系统以及文件进行操作。这种抽象的文件系统被称作虚拟文件系统。

2.虚拟文件系统(VFS)

虚拟文件系统相对于具体的如EXT2、EXT3、NTFS这样的文件系统不同之处在于“虚拟”二字。所以要理解虚拟文件系统,重点在于理解“虚拟”。
首先,虚拟文件系统在磁盘中是不占用存储空间的,它只活动于内存之中。对于一个具体的如EXT4文件系统,当用户需要使用该文件系统时就会将有关信息存入内存之中,即使用虚拟文件系统来进行操作。
如果内核要使用一个文件系统,第一步要做的是什么?当然是将该文件系统的超级块信息调入虚拟文件系统的超级块中。

VFS超级块

那么,我们来看一下虚拟文件系统中超级块的模样(部分):
这里写图片描述
这些信息存储在/include/linux/fs.h中,很明显,这个目录存储的不是某一具体的文件系统的信息。
之前我们说过,文件的i节点信息在未使用时“静静”的躺在磁盘中,当打开该文件,则需要使用相关信息。这时,文件的i节点信息会被调入内存中,填充的就是VFS的i节点。

VFS i节点

看一下虚拟文件系统中i节点结构的模样(部分):
这里写图片描述
这些信息同样存储在/include/linux/fs.h中。

dentry结构

实际上,一个文件除了被i节点描述,还被一个叫做dentry的结构所描述。在这里大家可能困惑,我之前说过一个文件是被一个i节点所描述的,这里却又说被一个dentry结构所描述,这不是自相矛盾了么?
实际上并不矛盾。我之前说文件被i节点描述指的是在具体的文件系统当中,指的是当文件“静静”躺在磁盘上时是被i节点所描述的。但对于虚拟文件系统,一个文件除了被i节点描述,还被dentry描述。而二者描述的角度是不同的。i节点描述了文件的固有属性,如文件大小、文件权限等。dentry描述了文件的逻辑属性,比如它的父目录是什么。
我们知道一个文件可以创建多个硬链接,硬链接的实质是不同的文件名指向同一个i节点,实际上这些不同的文件名的“本质”是一样的。所以,它们的i节点是相同的。然而,这些文件名可能处于不同的父目录中,那么它们的逻辑属性就不相同了,也就是说dentry是不同的。
对于i节点与dentry的区别,可以举个例子:
对于同一个出版社出版的某一图书,比如同济版的高数书,它有其固有属性如页数、版面大小、字数多少。对应i节点来描述。但同一本书可以出版多本,全国各地可能都有此书,那么,这些书分布的省份、书店、学校,这些就是逻辑属性。对应dentry来描述。
我们看一下dentry在内核中的定义:
这里写图片描述
在i节点的结构体中,有一个dentry的链表。
实际上,一个i节点可以对应多个dentry。

file结构

file结构是文件对象,linux在file结构中保存了文件打开的位置,称为打开的文件描述。这是一个与进程相关的文件结构,许多进程打开文件时对于文件的描述在其中被定义。其中具体的定义可查看源码中的定义:
这里写图片描述
定义在/include/linux/Fs.h中。

三、过程演示

使用mount命令
这里写图片描述
我们可以看到我的linux文件系统是EXT4,而U盘的文件系统是NTFS。
使用cp命令从U盘中复制一张图片到我的家目录下。
这里写图片描述
可以看到这个图片名叫0.png
结果当然是家目录下多了一个文件0.png
这里写图片描述

四、源码分析

上述演示过程看起来非常简单,只是使用cp命令指定一个文件和一个目录而已。
问题在于从我将U盘插入到传输数据再到我将U盘拔出。这期间操作系统做了哪些工作呢?
我的linux文件系统是EXT4,U盘的文件系统是NTFS。我们知道,我们将U盘插入计算机时,U盘会进行一个注册与安装的过程,也就是挂载。我的U盘挂载在了/run/media目录下。所以我的U盘的数据都可以从此文件夹下找到。

文件系统的注册、安装、卸载

那么,注册在源码中是如何实现的?

注册

内核在编译时就确定了其支持的文件系统,这些文件系统在系统启动时就在VFS中进行了注册。注册在内核中的实现是填写一个叫做file_system_type的结构体。该结构体中定义了文件系统类型名,文件系统特征,超级块的函数指针等。而且还定义了一个用于指向下一个file_system_type结构体的指针。
这里写图片描述
该结构体在/include/linux/fs.h中被定义。
如果需要安装的文件系统在linux中并没有注册呢?如果是内核可加载的模块,那么文件系统就会在实际安装的时候进行注册,而在卸载的时候进行注销。
实际上,该结构体的信息并不完整,我们都知道,要挂载一个文件系统,必须要指定一个挂载点,而这里并没有指明挂载点位于哪里。这就涉及下一个步骤,安装。

安装

文件系统的安装必须指定一个安装点。该安装点位于根文件系统的任一目录下。我的U盘安装在了/run/media下。把一个文件系统安装到一个安装点要用到的主要结构为mount
这里写图片描述
该结构体在/fs的Mount.h文件中被定义。

卸载

卸载文件系统就是将该文件系统的相关结构体从链表中释放。而在此之前,必须保证VFS中的超级块不为“脏”,而且没有该文件系统中的文件正在使用。卸载的内核实现是sys_umount。内核中源码位于fs/Namespace.c中,这里就不再详细介绍了。

不同文件系统间数据的传输

对于我上面演示的cp /run/media/pf/pf/0.png /home/pf命令,简单抽象的执行过程是这样的:
1.打开/run/media/pf/pf/0.png文件
2.在/home/pf文件下中新建0.png文件
3.读取/run/media/pf/pf/0.png文件数据到缓冲区
4.从缓冲区将数据写入/home/pf/0.png
5.关闭这两个文件
我们知道,我相互传数据的这两个文件系统是不同的,那么,不同的文件系统之间为什么可以用同一个系统调用来进行操作呢?
答案是linux的虚拟文件系统,那么内核中具体是如何实现的?
我这里以读取/run/media/pf/pf/0.png文件数据到缓冲区为例进行内核源码的分析。
首先,当用了read()系统调用,会进入内核态调用sys_read()。这个函数的实现源码如下:
这里写图片描述
我们可以看到,这里使用了宏定义SYSCALL_DEFINE3,这样就可以传递变长参数。这里的重点执行函数在于vfs_read()。也就是说接下来会调用这个函数继续执行操作。我们进入其定义。
这里写图片描述
前面三条都是判断语句。重点在于后面的判断语句:如果file->f_op->read不存在则执行do_sync_read()函数,若存在,则执行file->f_op->read()函数。
从这里可以很显然的看到,file与f_op都是结构体,file中定义了f_op,f_op中定义了read。现在,来看一下file结构体的样子。
这里写图片描述
之前在基础章描述过它,这里不再重复描述。这里的重点是要找到f_op。
我们可以看到它是一个file_operations类型的结构体。
file_operations想必大家都知道,这是一个文件操作的结构体,其中定义的都是文件操作的函数,也就是接口。内核中源码实现如下:
这里写图片描述
这个结构体也位于/include/linux/Fs.h中。
想必现在大家就明白了,f_op调用的read()其实就是这个结构中定义的函数read。但是,这里的read并没有具体的实现。对于不同的文件系统,它们可能就有不同的实现方案。例如我linux的EXT4文件系统对于file_operations的实现与我U盘NTFS文件系统对于file_operations的实现就不相同。
EXT4文件系统file_operations结构的具体实现:
这里写图片描述
定义在/fs/ntfs/File.c中。
NTFS文件系统file_operations结构的具体实现:
这里写图片描述
定义在/fs/ext4/File.c中。
可以看到,它们对于文件读、写等实现函数是相同的,但是对于有些函数如release、open等,实现函数则是不同的。
到此,大家可能就明白了linux文件系统是如何实现相同的系统调用实现对于不同文件系统的操作。实际上核心就是使用了file_operation这个接口实现的。

五、总结

实际上,在linux中有许多非常巧妙的设计,而且这些巧妙的设计也确实有其必然性。重要的是要学会透过现象看本质,抓住一般规律。本篇博客对于linux文件系统的分析还不算深入,今后还会进一步进行深入学习。

简述TCP/IP协议分层模型

一、TCP/IP与OSI参考模型

这里写图片描述

二、简述

由上图我们可以看到,TCP/IP协议分层模型与OSI参考模型十分相似。之前我介绍过OSI参考模型每层的目的。TCP/IP的每一层都对应有协议,如果我们了解了协议处于其中的哪一层,那么我们就可以对协议的目的有所了解。然后对于每个协议的具体技术要求就可以参考相应的规范了。这里不详述协议本身的协议,就讲述一下各个协议与OSI参考模型中各个分层的对应关系。

三、硬件(物理层)

TCP/IP的最底层是负责数据传输的硬件,如以太网或者电话线路等物理设备。TCP/IP是在网络互联的设备之间能够通信的前提之下提出来的协议。

四、网络接口层(数据链路层)

网络接口层利用以太网中的数据链路层进行通信,因此属于接口层。也可以把它当作让NIC起作用的“驱动程序”。

五、互联网层(网络层)

互联网层使用IP协议,相当于OSI模型中的网络层。
TCP/IP分层中的互联网层和传输层的功能通常是由操作系统提供。尤其是路由器,它必须得实现通过互联网层转发分组数据包的功能。此外,链接互联网的所有主机和路由器都必须实现IP的功能,其他连接互联网的网络设备就没必要一定实现IP或TCP的功能。

IP

IP是跨网络传送数据包,使整个互联网都能收到数据,这期间它使用IP地址作为主机的标识。它不具备重发功能,所以,就算是数据未能发送至目的地址,它也不会重发。所以它属于非可靠性传输协议。

ICMP

当由于一些异常情况,数据未能发送到目的地址时,ICMP协议会给发送端返回一个通知。它有时也用来作诊断网络的健康状况。

ARP

从分组数据包的IP地址中解析出物理地址(MAC地址)的一种协议。

六、传输层

传输层最主要的功能就是要实现应用程序之间的通信。计算机的内部,通常同一时间运行着多个程序。为此,我们必须分清哪些程序与哪些程序正在进行通信,识别这些应用程序的是端口号。

TCP

TCP是一种面向有连接的传输协议。优点在于安全性高,缺点在于多次发包收包会浪费网络流量。

UDP

UDP是一种面向无连接的传输协议。优点在于程序简单,缺点在于安全性低。

七、应用层(会话层以上的分层)

TCP/IP的分层中,将OSI参考模型中的会话层、表示层、应用层的功能都集中到了应用程序中实现。
我这里简单介绍几个应用程序:

WWW

浏览器与服务器之间通信所用的协议是HTTP,它属于OSI应用层的协议。
所传输数据的主要格式是HTML,它属于OSI表示层的协议。

电子邮件

发送电子邮件所用到的协议是SMTP,它只可以发送文本格式,后来电子邮件的格式由MIME协议扩展,就可以发送声音、图像等各种各样的信息。MIME也属于表示层的协议。

文件传输

文件传输指可以将其他计算机硬盘中的文件传输到本机上,或者相反的操作。文件传输所用的协议是FTP。

远程登录

远程登录是指登录到远程的计算机上,使那台计算机上的程序可以运行。
TCP/IP网络中远程登录常用TELNET和SSH两种协议。

网络管理

在TCP/IP中进行网络管理时,采用SNMP协议。使用SNMP管理的主机、网桥、路由器等称作SNMP代理,而进行管理的那一段叫做管理器。SNMP就是管理器和代理之间要用到的协议。

Java多线程学习笔记(三)

一、背景

这篇博客想要讲解的是线程的暂停与停止。暂停与停止的方法各有其特点与优缺点,我会在这里逐一进行分析,并且大都是用举例子的方式来进行阐述。

二、线程的暂停

suspend和resume

线程的暂停使用的是suspend和resume方法,我写了程序进行验证。
这里写图片描述
这里写图片描述

这段代码运行出来的结果如下:
这里写图片描述
可见suspend和resume的功能:suspend可以使一个线程暂时停止运行,resume可以恢复被暂停的线程。

suspend和resume“独占”的缺点

就是说,如果被挂起的线程正在访问临界资源,那么这个时候其它线程也无法对该临界资源进行访问,也就使“独占”一词的来源。
下面举一个有趣的例子:
先来看这段程序:
这里写图片描述
这里写图片描述
这段程序十分简单,很明显,最后输出的结果应该使MAIN END!!!!!
没错,输出结果如下:
这里写图片描述
那么,我们再来看这段程序:
这里写图片描述
这里写图片描述
可以发现,这段程序与上一段程序唯一的不同在于我再MyThread类run方法中添加了一个将i值打印出来的语句。按道理讲,这段代码的输出应该是将i的值不断的输出,知道该线程被挂起,然后输出MAIN END!!!!!
但是事实上,输出结果的结果并没有MAIN END!!!!!
结果如下:
这里写图片描述
这是为什么呢?
我们来看一下println的源码:
这里写图片描述
这下你或许明白了,因为由关键字synchronized,所以这段代码是临界区,在MyThread线程在对其进行访问的时候被暂停了,那么,其它线程当然也无法访问这段代码,所以main中的println迟迟无法打印出结果来。这就是suspend与resume的“独占”缺点。

三、线程的停止

interrupt、interrupted、isInterrupted

interrupt

要介绍这三种方法,先来看一段代码:
这里写图片描述
这里写图片描述
这段代码的运行结果如下:
这里写图片描述
也就是说,从1一直输出到500000,而interrupt方法好像没有起什么作用,
事实上并不是的,interrupt方法只是给该线程发出了一个停止的信号,至于怎么使用这个信号,就要看interrupted和isInterrupted了。因为interrupted和isInterrupted可以用来判断一个线程是否有处于停止状态,判断出来停止状态就可以使用if语句来进行适当的操作。

interrupted

这里写图片描述
这里写图片描述
运行结果如下:
这里写图片描述
为什么测试出来的是否由停止信号还是false呢?
原因在于interrupted测试的是当前线程,也就是这段程序中的main线程是否停止,当然main线程没有停止,所以interrupted返回的是false。
那么我再将源程序改一下,将main也用interrupt来停止一下:
程序如下:
这里写图片描述
这里写图片描述
这时的运行结果如下:
这里写图片描述
如结果所示,返回的第一个值是true,也就是说检测到了main有停止信号,但是为什么第二个值又是false呢?
原因在于interrupted这个方法在输出true之后会将停止状态清除掉,那么第一个方法将停止状态清除掉了,自然而然第二个interrupted返回的是false。

isInterrupted

接下来我们来看一下isInterrupted方法。
isInterrupted的方法返回值不同于interrupted。其可以判断其它线程的停止状态,而且它不会清除停止状态。
示例程序如下:
这里写图片描述
这里写图片描述
运行结果如下:
这里写图片描述

break,return,throw new InterruptedException()

break、return和throw new InterruptedException()可以与interrupted或isInterrupted的if语句搭配来使用
我这里只演示一下break的用法,return、throw new InterruptedException()类似
这里写图片描述
这里写图片描述
运行结果如下:
这里写图片描述
这里需要注意的是,虽然break和return、throw new InterruptedException()的用法类似,但是实际上break只是跳出了循环而已,并没有真正的结束线程,而return、throw new InterruptedException()可以真正的结束线程,另外throw new InterruptedException()会将程序跳转至run方法中的catch。这样可以对异常信息进行相关处理,而且不至于使程序中出现很多break和return造成污染。

沉睡中停止线程

如果在沉睡中停止线程会出现什么情况呢?
看如下程序:
这里写图片描述
这里写图片描述
结果如下:
这里写图片描述
看到这个结果我们可以得出来两个信息:
1.线程在沉睡中如果被停止就会直接进入catch中
2.进入catch语句之前会清除停止状态值

那么,如果是先停止线程,然后线程要进入睡眠会出现什么情况呢?
示例程序如下:
这里写图片描述
这里写图片描述
结果如下:
这里写图片描述
可以看到,与上面类似,仍旧是进入catch,并且会清除停止状态值。

stop

stop这个方法十分暴力,用的时候需谨慎。
我这里写了一个示例程序如下:
这里写图片描述
这里写图片描述
输出结果如下:
这里写图片描述
可以看到,正在运行着的线程直接就被停止掉,stop方法看起来十分简单易用,实际上则会出现一些问题。比如stop会释放锁,这会造成数据不同步的不良后果。而且stop会让线程停止,则有可能一些清理工作得不到完成.

四、总结

这篇博客主要讲解了Java多线程中一些在暂停与停止线程中常用的方法还有它们的用途以及优缺点。其中,stop、suspend、resume在使用时需慎重,因为它们有可能会引起某些麻烦。

OSI参考模型

OSI参考模型

这里写图片描述
OSI参考模型共分七层,接下来我举例讲解每一层具体的作用。

一个例子

假设现在有两台主机A和B,A主机的用户给B主机的用户发送一封邮件,内容是“你好”。
这里写图片描述
主机A与主机B都采用的OSI分层模型。发送方的数据从应用层一层一层传数据到物理层,发送到B后又由相反的顺序,即从物理层向应用层一层一层传数据。最后用户B可以在应用层看到发送的数据“你好”。
接下来分层来讲解每一层的大概作用:
1.应用层:编写内容“你好”,输入发送方信息与接收方信息。该协议会在所要传送的数据前加一个首部信息。到对方应用层后,会进行相反的操作,然后将信息存储,若内存不足无法存储会返回错误信息,这也是在应用层。
2.表示层:由于异构计算机对信息的编码方式是不同的,这就可能导致不兼容的问题,所以,在表示层将数据都转换为“统一的网络数据格式”,表示层与表示层之间因为要识别编码格式也会加首部信息。
3.会话层:假设你发送了两份邮件,那么,是建立一次链接将两份都发送呢,还是建立一次链接发送一个之后断开,再建立一次链接再发送一份呢?也就是说如何建立链接,何时建立链接,何时断开链接,这些是由会话层负责,会话层在将信息传给下一层之前也会添加首部信息。
4.传输层:传输层负责建立链接与断开链接。另外,如果你发送的一份邮件遇到了某种错误而没由发过去,也是由传输层负责重新建立链接发送的,简单的说,就是传输层保证了数据安全完整的传输。也会为数据添加首部以识别信息。
5.网络层:网络层负责将数据发送至最终的目的地址。但其需要与传输层配合以将数据安全完整的送达。
6.数据链路层:数据链路层负责每一个区间内的通信,网络层与数据链路层都是基于目标地址将数据发送给接收端的,但是网络层负责将数据发送给最终目的地址,而数据链路层负责发送一个分段内的数据。
7.物理层:物理层将数据的0、1转换为电压和脉冲光传输给物理的传输介质,相互直连的设备使用地址实现传输功能。

主机B所做的事情与A正好是相反的,它从物理层开始将收到的数据逐层发送给上一层进行处理,直至安全传输到应用层显示在界面上出现”你好”。