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的。版本不同可能某些内核函数是不同的。但如果版本比我高的内核基本上是没问题的。