ext4日志系统分析(一)

一、背景

ext4文件系统用的是jbd2日志系统。本篇博客从内核源码着手,大致分析jbd2日志系统。

二、源码分析

我这里用的是source insight工具来分析linux内核的源码。
内核版本为3.10.0。
首先我们搜索一下jbd2关键字。可以看到如下图所示:
这里写图片描述
可以看到,在fs/jbd2中存在6个文件。这个fs/jbd2中存的就是jbd2日志系统的代码。
一步一步来,先看看插入jbd2模块时系统做了什么工作。
插入模块的函数在Journal.c中被定义。位于2611-2640行。
(这个图片下面经常要用到,用到时记得往回翻看!)
这里写图片描述
插入模块时运行的是journal_init函数,退出模块时运行的是journal_exit函数。我们先来看看插入模块时初始化函数journal_init做了什么。

第2615行:

宏定义检查journal_superblock_s结构体的大小是否等于1024字节。
若不等于1024字节,就会报错。
可以进入这个结构体看看,
这里写图片描述
0x0400的大小表示这个结构体就是1024字节。如果这个结构体不是1024字节难道不该报错么?
这个结构体应该表示的是日志系统的固有属性。

第2617行:

journal_init_caches()函数
初始化日志所要用到的缓冲区,看看这个函数。
这里写图片描述
这个函数中调用了四个函数,都是用来初始化cache的。
分别为:
revoke_cache
head_cache
handle_cache
transaction_cache
以revoke_cache为例,我们看看它的函数
这里写图片描述

208-209:

断言jbd2_revoke_record_cache和jbd2_revoke_table_cache一定是空的。非空则报错。

211:

给revoke_record创建cache,使用SLAB分配机制。

213:

创建cache失败则跳到222销毁revoke_cache。

216:

给revoke_table创建cache,使用SLAB分配机制。

218:

创建cache失败则跳到224返回错误信息。
接下来我们回到journal_init函数继续来讲插入journal模块时运行的函数
这个时候假设cache已经创建成功了。会给ret返回0
接下来运行的就是jbd2_create_jbd_stats_proc_entry函数。

2619(往上翻我刚才说的那个图)

这个函数用来在/proc/fs下创建jbd2目录。jbd2用来存储日志的状态信息。
至于这些状态信息从哪里来的以后再说。这里只是创建了这个目录。
我们可以具体的看一下这些源码:
这里写图片描述
我们可以看到有两个jbd2_create_jbd_stats_proc_entry函数
一个是在/pro/fs下创建jbd2,一个是空循环函数。看2535、2550、2555可以知道这是内核中常用到的条件编译。#ifdef CONFIG_PROC_FS表示如果proc文件系统则编译下面的这段代码,否则,没有/proc目录,那么肯定也就无法创建jbd2,则执行空循环函数。
好了,接着讲journal_init函数。
之前我们说到,如果返回的ret等于0则表示初始化cache成功,接着会创建目录,那如果ret不等于0,也就是说cache的初始化失败了呢?(忘了ret是干吗的可以翻看前面第2个图片的代码)
这时就会执行jbd2_journal_destroy_caches函数

2621(往上翻我刚才说的那个图)

很明显,这个函数是用来销毁cache的,也就是说,cache创建失败了,那么严谨起见,不能就这么退出了,要先将cache销毁掉再报错。
看下该函数:
这里写图片描述
之前创建的四个cache在这里都要被销毁。那么,最后一个destroy_slabs函数是干什么的呢?我们可以进入这个函数看一下究竟。
这里写图片描述
为什么我连着上面一大段解释也要截图出来呢?是因为仅仅看此函数是看不出来究竟的。大家可以阅读上面的解释,那么此函数的作用就一目了然。
2171行可以看到,这些cache是在挂载时开辟出来的,用于复制缓冲区的数据。而在jbd2卸载时这些cache将被释放。也就是这个函数用来释放这些cache。
journal_init函数解释到这里先告一段落。
接下来我们看一下卸载模块时运行的函数journal_exit。
这里写图片描述
这里有一个条件编译,如果是JBD2的调试模式运行2629-2631,否则只运行其余部分。endif后面的代码就不用过多解释了,也就是删除目录与刚才讲过的cache的销毁。

2628

这段代码表示编译时是JBD2的调试模式。

2629

这是对nr_journal_heads的一个读原子操作。
我们看看这个变量。
这里写图片描述
这里写图片描述
2289行定义了这个变量是原子变量,初值为0
2326行表明,如果分配了一次journal_head,那么就会对它进行原子操作加1
2343行表明,如果释放了一次journal_head,那么就会对它进行原子操作减1
之前提过journal_head_cache,以后再细讲。

2631

这段代码表明,JBD2日志系统泄漏了n个journal_head。n是上面从nr_journal_heads中读出的数值。

三、总结

到这里jbd2模块装载与卸载时做了什么也就大致分析完了。接下来的博客会继续分析ext4的日志系统。

内核中的同步

一、内核中同步的问题

假设我们把内核比作一个服务器(我是说假设),那么正在CPU上运行的进程、发出中断请求的外部设备就相当于一个客户端,客户端不断访问服务器,时间也不一定。就像是服务器会相应客户端的请求一样,内核也会响应进程与外设的请求。但同样,时间也不一定,请求的顺序也不一定。内核中有许多的共享资源,这些资源可以被不同进程所访问。这时候就会出现一个“管理”的问题。就像是大学中的班级占用教室上课一样也需要管理,如果不进行妥善管理,那么就有可能出现一个班级正在上课而另一个班级的学生也要用这个教室,这个时候就会出现冲突。而教务处就起着管理的作用,由教务处安排教室在什么时间被什么班级占用。这里的教务处就像是内核,班级就像是访问共享资源的进程,教室就像是内核中的共享资源。

二、临界区

临界区就是访问和操作共享数据的代码段。
要注意临界区指的是代码段。
我这里举个具体的例子来说:
假设有内核任务a与内核任务b,这两个任务都访问同一个共享资源变量i,
执行i++操作。
学过微机原理的各位应该知道这个i++大体上是分为三步进行的:
1)得到当前变量i的值并且拷贝到一个寄存器中。
2)将寄存器中的值加1。
3)把i的新值写回到内存中。
假设i的初值为1。当任务a、b并发执行i++操作时,期望的结果应该是一个任务完成1)2)3)之后,另一个任务再进行步骤1)2)3)。这样得出的结果应该是i=3。可实际上,有可能是这样的情况:
任务a执行1)->任务b执行1)->任务a执行2)->任务b执行2)->任务a执行3)->任务b执行3)
这样执行下来的结果是i=2。
为什么会产生这样的结果呢?因为任务a加好的结果还没有放回内存中,任务b从内存中取出的i值还是初值1。
这里的i++实际上就是一个最简单的临界区的例子。它是一个代码段,是一个访问和操作共享数据i的代码段。
任务并发的操作共享数据可能导致意想不到的结果。甚者,我们都知道内核中有各种各样的链表结构,如果并发的操作链表则可能会破坏链表的结构,结果更是可怕。所以,我们应该思考如何来保护这些对象呢?如何来管理并发任务对共享数据的访问呢?

三、内核同步措施

我这里只简要的介绍三种内核同步措施。

1 原子操作

Linux内核提供了一些专门的函数和宏,这些函数和宏作用于atomic_t类型的变量。具体的可以查阅相关资料。

2 自旋锁

自旋锁是专为防止多处理器并行而引入的一种锁。
自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被持有的自旋锁,那么这个任务就会一直进行忙循环,也就是旋转,等待锁的重新可用。如果锁没有被持有,那么请求它的内核便立即得到它并且继续执行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区。因此这种锁可有效的避免多处理器上并行运行的内核任务竞争共享资源。具体自旋锁的使用可以查阅相关资料。

3 信号量

信号量是一种睡眠锁,也就是说,它也是一种锁机制,不同于自旋锁,当等待资源的任务没有持有锁时选择睡眠等待而不是忙循环的访问。于是,对比于自旋锁,信号量更适合用于当需等待的时间较长时。因为这样就不会降低CPU的效率来不断循环。关于信号量的使用我下面会做一个实例。

四、生产者-消费者并发实例

对于并发访问与信号量的使用我这里举一个具体的例子:
想必大家都喝过牛奶,那么,生产牛奶的厂家和经销商之间是如何进行分工合作的呢?简单来说,牛奶厂家和经销商最希望的就是厂家生产一批牛奶,经销商刚好需要的就是这么多的一批牛奶。厂家第二天再生产一批牛奶,经销商需要的还是这么多的一批牛奶。以后的日子每天如此。当然,这是理想的情况,现实不会如此。现实中厂家生产的多了,经销商不需要这么多这就是供大于求。会造成浪费。而厂家生产的少了,经销商卖完了没东西卖,这就是供不应求,钱就赚的少了,效益没有最大化。总之都不是理想情况。
这里我们将此抽象为生产者-消费者模型,厂家(producer)将牛奶放入仓库(缓冲区),经销商(consumer)将牛奶从仓库(缓冲区)中取走去售卖。
话不多说,代码如下:

#include<linux/kthread.h>
#include<linux/init.h>
#include<linux/module.h>
#include<linux/semaphore.h>
#include<linux/sched.h>
#include<asm/atomic.h>
#include<linux/delay.h>
#define PRODUCT_NUMS 10

static struct semaphore sem_producer;
static struct semaphore sem_consumer;

static char product[12];
static atomic_t num;
static int producer(void *product);
static int consumer(void *product);
static int id = 1;
static int consume_num = 1;

static int producer(void *p)
{
  char *product = (char *)p;
  int i;

  atomic_inc(&num);
  printk("producer[%d] start...\n",current->pid);
  for(i = 0;i<PRODUCT_NUMS;i++){
    down(&sem_producer);
    snprintf(product,12,"2017-04-%d",id++);
    printk("producer[%d]produce %s\n",current->pid,product);
    up(&sem_consumer);
    if(id == 10)
      msleep(400);
  }
      msleep(400);
  printk("producer[%d] exit...\n",current->pid);
  return 0;
}

static int consumer(void *p)
{
  char *product = (char *)p;

  printk("consumer[%d] start...\n",current->pid);
  for(;;){
    down_interruptible(&sem_consumer);
    if(consume_num >= PRODUCT_NUMS * atomic_read(&num))
      break;
    printk("consumer[%d] consume %s\n",current->pid,product);
    consume_num++;
    memset(product,'\0',12);
    up(&sem_producer);
  }
  printk("consumer[%d] consume %s\n",current->pid,product);
  printk("consumer[%d] exit...\n",current->pid);
  return 0;
}

static int procon_init(void)
{
  printk(KERN_INFO"show producer and consumer\n");
  sema_init(&sem_producer,1);
  sema_init(&sem_consumer,0);
  atomic_set(&num,0);
  kthread_run(producer,product,"conpro_1");
  kthread_run(consumer,product,"compro_1");
  return 0;
}

static void procon_exit(void)
{
  printk(KERN_INFO"exit producer and consumer\n");
}

module_init(procon_init);
module_exit(procon_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("producer and consumer Module");
MODULE_ALIAS("a simplest module");

这是一个内核模块,所以需要用Makefile文件进行编译模块。我这里把Makefile文件内容也写在这里:
obj-m := procon.o
CURRENT_PATH := $(shell pwd)
LINUX_PATH := $(shell uname -r)
LINUX_KERNEL_PATH := /usr/src/kernels/$(LINUX_PATH)

all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean

写好这两个程序后,使用make编译模块,
insmod procon.ko加载模块,然后用dmesg来观察结果。
我的结果是这样的:
这里写图片描述
噢,忘了说了,我的内核版本是3.10.0的。版本不同可能某些内核函数是不同的。但如果版本比我高的内核基本上是没问题的。

使用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正好是相反的,它从物理层开始将收到的数据逐层发送给上一层进行处理,直至安全传输到应用层显示在界面上出现”你好”。

Java多线程学习笔记(二)

this和Thread.currentThread()的区别

我们先来看一下我写的一个试验程序:

这里写图片描述

这两段代码的运行结果如下:
这里写图片描述
在c.start();之前,只有一个main线程在运行。我们先运行的CountOperate的构造函数。那么为什么this.getName和Thread.currentThread().getName的运行结果不同呢,分别是Thread-0和main,很显然,现在只有一个线程main在运行,所以解释是,this是继承的子类的线程对象,Thread.currentThread()是当前正在运行的线程对象。也正因如此,this.isAlive的结果是false,Thread.currentThread().isAlive的结果是true,原因是此时Thread-0线程没有在运行中,而main线程在运行中。

Java多线程学习笔记(一)

一、什么是多线程技术?

    线程也可以叫做轻量级进程。是一个程序执行流的最小单元。或许这么讲还不够好理解。打个比方:我们在电脑上登陆了QQ,那么QQ是一个进程单元。线程是什么呢?线程就是我们跟别人聊天、下载别人传送来的文件或者跟好友视频着。大家都知道,我们是可以用QQ一边跟好友视频、一边打字聊天、一边下载文件的,这就是多线程技术。这样我们就节省了很多等待的时间。这就是多线程技术的好处。

二、一个简单的多线程程序

    看下面的这个程序我们可以看到,类MyThread继承Thread,那么类MyThread也可以用Thread的方法,在这里我们用了start方法。

    Thread类中的start方法就是通知“线程规划器”,告诉它这个线程已经准备好了,等待调用线程对象的run()方法。

    两个线程并发执行,所以输出的结果的先后顺序是不一定的。结果如下:

    那么如果我们不使用Thread中的start方法,而是使用run方法呢?肯定还是调用run方法无疑,结果会是如何呢?​

    我们可以看到,这是顺序执行的,由run=main可以看到,实际上这还是一个线程main。

    这就能看出来调用start和调用run的区别在于:

    调用run就是像一般的调用类的方法那样,等它顺序执行完了才会执行下个方法。

    而调用start实际上是开始一个新线程,将此线程交给“线程规划器”来处理了。所以是两个线程并发执行。

三、共享数据的问题

    共享数据的情况就是多个线程访问同一个变量。这种情况会经常常见的,比如说投票机制,需要对同一个变量进行加一。

    但是共享数据的时候是会出现一些问题的。如下例:​

结果如下:​

要解决这个问题只需要在被调用的方法前加关键字:synchronized

这就给该段代码加锁了,被加锁的区域叫做临界区,一次只能有一个线程访问。

结果如下:​

看到这个结果可能有人会有问题,为什么不是按照ABCDE的顺序输出呢?

实际上执行start方法的顺序不代表线程启动的顺序。

四、实现Runnable接口

我们知道,java是单根继承的,所以如果一个类已经有了父类了就不能在继承Thread了,

这种情况下就需要使用Runnable接口来实现多线程。程序如下:

五、总结

    这篇博客写了一些简单的多线程程序,分析了多线程共享资源时可能出现的问题,然后讲了Runnable接口,当类已经有父类时可以使用Runnable接口来开始一个新线程。

初次接触linux系统编程

一、背景

第一次接触linux系统编程,先从最简单的做起,首先探索一下more命令的实现方法。more命令使用起来并不复杂,先在命令行中输入more,会得到下面的反馈信息。

即more的使用方法是more+选项+名称。我在这里先忽略掉选项,只实现more来分页查看文件内容。(只是more最简单的查看文件内容的功能)

二、实现过程

1.more的运作方式

想实现more的功能,先看一下more的运作方式。在这里拿/etc/passwd以作演示。查看的过程这里就不截图了。首先如果文件无法打开的话(如果文件有问题,fopen()函数会返回空指针。磁盘满、文件名非法、存取权限不够或者硬件问题都可能导致文件打不开),必须要在屏幕标准输出一段警告语来警告文件无法打开。如果文件可以正常打开,那么more命令会首先显示文件的部分信息,同时会给出一个More的反白的字符并且后面跟有查看的百分比。此时若输入(1)q:会退出查看页面(2)空格:会显示部分信息(与第一次显示的行数相同)(3)Enter:会显示一行信息。

2.初次实现

第一次尝试系统编程,对照more的功能编出了这样的程序。说起来不好意思,其中主函数main的显示参数及其个数的用法我是第一次使用,之前在C的学习中并未见过如此用法,可见自己真是疏于实践了,这么有用的东西之前都没有掌握好,在这里通过这个实践很快就掌握其用法了。这段代码的功能还很残缺。几个比较大的问题:1.没有提示more的反白醒目字体。2.在q与space键入后,必须按Enter键才可以继续执行命令。3.Enter命令键入后存在问题:文件信息是隔行显示的。

3.修正

​这次修改添加了more的反白提示字。并且修正了之前的用Enter键隔行显示的bug。添加反白提示字很简单,只需要在see_more函数中添加一段代码。而隔行显示是因为在more函数中多了一行else命令,导致并未输出接下来的一行而被下一行在line中取代了。

三、总结

这段程序使我对系统编程有了一点点的概念。就这种入门级的编程都耗费了我很多精力去编写,因为代码看上去都不是很难去理解,但自己去写可真是漏洞百出,特别头疼。而且最终写出的程序还很不如人意,接下来还有许多东西要去学习来完善它。但与此同时,我也变得不像之前对linux系统编程那样感到望而生畏遥不可及了。只要沉下心学习,总会有所收获。