Archive for the ‘Kernel内核分析’ category

打印VFS中的结构体

October 22nd, 2014

通过打印VFS结构体,我们可以快速掌握VFS主要结构体之间的关系

详见http://www.lizhaozhong.info/archives/1080

我之前在网上找了许多资料,都是关于linux 2.X的,有个问题在与inode中的i_dentry在linux 3.X中以hlist形式出现,我们都知道hash list比其一般的list_head查找速度更快。尤其是在大规模的链表中,具体的hlist定义在include/linux/list.h中。

他们都是一个个宏函数:

#define hlist_entry(ptr, type, member) container_of(ptr,type,member)

#define hlist_for_each(pos, head) \
 for (pos = (head)->first; pos ; pos = pos->next)

#define hlist_for_each_safe(pos, n, head) \
 for (pos = (head)->first; pos && ({ n = pos->next; 1; }); \
 pos = n)
.......

用法与list差不多http://www.lizhaozhong.info/archives/951

这里就不一一说明了。

我们要知道从super_block到inode,然后从inode寻找dentry是可行的。inode与dentry本来就是互通的。

包括从task_struct指向struct file -> dentry->inode都是可行的,比较灵活.

这里要说明的是前面那4个宏定义,每个kernel版本都是不同的,需要用注释命令进行查看。

#define SUPER_BLOCKS_ADDRESS 0xffffffff81c72cb0//  $cat /proc/kallsyms | grep super_block
#define SB_LOCK_ADDRESS 0xffffffff81fd5fa0// cat /proc/kallsyms | grep sb_lock
#define FILE_SYSTEM_ADDRESS 0xffffffff81fd6b88
#define FILE_SYSTEM_LOCK_ADDRESS 0xffffffff81fd6b80

int traverse_superblock(void)
{
	struct super_block *sb;
	struct list_head *pos;
	struct list_head *linode;
	struct inode *pinode;
//	struct hlist_head *ldentry;
	struct dentry *pdentry,*parents;
//	char *buffer= kmalloc(sizeof(char)*10000,GFP_KERNEL);	

	unsigned long long count = 0;
	printk("print some fields of super blocks:\n");

//	if(buffer==NULL)
//		return -ENOMEM;
	spin_lock((spinlock_t *)SB_LOCK_ADDRESS);

	list_for_each(pos,(struct list_head *)SUPER_BLOCKS_ADDRESS)
	{
		sb=list_entry(pos,struct super_block,s_list);
		printk("dev_t:	%d,	%d\n",MAJOR(sb->s_dev),MINOR(sb->s_dev));
		printk("fs_name:	%s\n",sb->s_type->name);

		list_for_each(linode,&sb->s_inodes)
		{
			pinode = list_entry(linode,struct inode,i_sb_list);
			count++;
			printk("%lu[",pinode->i_ino);

//			pdentry = d_find_alias(pinode);
			hlist_for_each_entry(pdentry,&pinode->i_dentry,d_alias)
			{
				parents = pdentry;
				while (!IS_ROOT(parents))
				{
					printk("%s->",parents->d_name.name);
					parents = parents->d_parent;
				}
				//memset(buffer,'\0',sizeof(buffer));
				//buffer = dentry_path_raw(parents,buffer,sizeof(buffer));
				//printk("%s",buffer);
			}

			printk("/]\n");
		}

		printk("\n");
	}
	spin_unlock((spinlock_t *)SB_LOCK_ADDRESS);
	printk("the number of inodes: %llu\n",sizeof(struct inode *)*count);
}

static int print_init(void)
{
	struct file_system_type **pos;
	printk("\n\nprint file system_type:\n");

	read_lock((rwlock_t *)FILE_SYSTEM_LOCK_ADDRESS);
	pos	=(struct file_system_type **)FILE_SYSTEM_ADDRESS;

	while(*pos)
	{
		printk("name: %s\n",(*pos)->name);
		pos = &((*pos)->next);
	}

	read_unlock((rwlock_t *)FILE_SYSTEM_LOCK_ADDRESS);
	return 0;
}

static int __init traverse_init(void)
{
//	print_init();
	traverse_superblock();
	return 0;
}

这里我们要阐述一个问题,比如我们想得到一个dentry的fullpath,需要一直向上遍历d_parent。判断是否到root path ,就是判断他是否是指向自己就可以了。
struct file_system_type **pos是一个指针数组,每个元素长度都是不同的。

 

 

dmesg.log

 

 

VFS Data Structure关系(1)

October 20th, 2014

 VFS是管理具体文件系统的接口,Linux可支持数十种文件系统,不同的文件系统可以同时共存于一个系统之中。这些不同类型的文件系统并不是各自封闭的,会进行文件复制和移动等。

VFS是在各种具体的文件系统之上建立了一个抽象层,它屏蔽了不同文件系统间的差异。它之所以可以将各种文件系统纳入其中,是因为它提供了一个通用的文件系统模型。

VFS主要通过一组数据结构来描述文件对象。其中有四个基本的结构体:

超级块(struct super_block):它描述一个已安装了的文件系统。

索引结点(struct inode):它描述一个文件。

目录项(strcut dentry):它描述文件系统的层次结构。一个完整路径的每个组成部分都是一个目录项。比如打开/home/lzz/code/hello.c时,内核分别为/,home/,lzz/,code/,hello.c创建相应的目录项。

文件(struct file):它描述一个已被进程打开的文件。

位置:

block

VFS采用面向对象的思想,在上述每一个结构体中即包含描述每个文件对象属性的数据,又包含对这些数据进行操作的函数指针结构体。也就是说,上述四个基本的结构体中,每一个结构体中又嵌套了一个子结构体,这个子结构体包含了对父结构体进行各种操作的函数指针。

struct dentry (include/linux/dcache.h)

为了方便对目标文件的快速查找,VFS引入了目录项。目标文件路径中的每一项都代表一个目录项,比如/home/test.c中,/,home,test.c都分别是一个目录项。这些目录项都属于路径的一部分,并且每个目录项都与其对应的inode相联系。如果VFS得到了某个dentry,那么也就随之得到了这个目录项所对应文件的inode,这样就可以对这个inode所对应的文件进行相应操作。所以,依次沿着目标文件路径中各部分的目录项进行搜索,最终则可找到目标文件的inode。

与超级块和索引结点不同的是,目录项在磁盘上并没有对应的实体文件,它会在需要时候现场被创建。因此,在目录项结构体中并没有脏数据字段,因为目录项并不会涉及重写到磁盘。

d_inode:与该目录项相关联的索引结点;

struct super_block (include/linux/fs.h)

超级块结构代表一个已经安装了的文件系统,其存储该文件系统的有关信息。对于一个基于磁盘的文件系统来说,这类对象存放于磁盘的特定扇区中;对于非基于磁盘的文件系统,它会在该文件系统的使用现场创建超级块结构并存放在内存中。

s_inode 代表该文件系统中所有的索引结点形成一个双联表,该字段存放这个链表的头结点;

s_files:该文件系统中所有已被打开的文件形成一个双联表,该字段存放这个链表的头结点;

struct file(include/linux/fs.h)

每当一个进程打开一个文件时,内存中有会有相应的file结构体。因此,当一个文件被多个进程打开时,这个文件就会有多个对应的文件结构体。但是,这些文件结构体对应的索引结点和目录项却是唯一的

struct inode (include/linux/fs.h)

索引结点结构体用来描述存放在磁盘上的文件信息。每当内核对磁盘上的文件进行操作时,就会将该文件的信息填充到一个索引结点可以代表一个普通的文件,也可以代表管道或者设备文件等这样的特殊文件

i_sb_list:每个文件系统中的inode都会形成一个双联表,这个双链表的头结点存放在超级块的s_inodes中。而该字段中的prev和next指针分别指向在双链表中与其相邻的前后两个元素;

i_list:VFS中使用四个链表来管理不同状态的inode结点。inode_unused将当前未使用的inode链接起来,inode_in_use将当前正在被使用的inode链接起来,超级块中的s_dirty将所有脏inode链接起来,i_hash将所有hash值相同的inode链接起来。i_list中包含prev和next两个指针,分别指向与当前inode处于同一个状态链表的前后两个元素。

VFS

 

关于此图的说明:

1. 图中平行结构之间的箭头表示这两者之间仍有有若干个类似结点相连接;

PS:在linux 3.14.8 中,从task_struct到struct file有一个过程:

task_struct -> struct files_struct -> struct file -> struct path -> struct dentry

2. 图中阴影部分所示的进程并不是以实际的关系来表示的;

3. 图中彩色线条示意的场景:三个进程分别打开同一个文件。进程1和进程2打开同一路径的文件,因此两者的打开文件对应同一个目录项;而进程3打开的是一个硬链接文件,因此对应的目录项与前两者不同;

4.索引结点中i_sb_list链表是链接一个文件系统中所有inode的链表,因此相邻的inode之间均会由此链表链接;而i_list链接的是处于同一个状态的所有inode。所以,相邻inode之间并不一定链接在一起。

 

Linux进程的虚拟地址空间(笔记)

September 16th, 2014

进程虚拟空间是个32或64位的“平坦”(独立的连续区间)地址空间(空间的具体大小取决于体系结构)。

为了方便管理,虚拟空间被划分为许多大小可变的(但必须是4096的倍数)内存区域,如果要查看某个进程占用的内存区域,可以使用命令cat /proc/<pid>/maps获得.pid是进程的id号

» Read more: Linux进程的虚拟地址空间(笔记)

wait.h解析与应用

September 11th, 2014

list_head结构那样,等待队列(wait queue)作为linux内核中的基础数据结构,与进程调度紧密结合在一起;在驱动程序中,常常使用等待队列来实现进程的阻塞和进程的唤醒。 数据结构 一般我们的链式线性表都会有一个头结点,以使我们迅速找到这个线性链表的“领导”。在等待队列中,同样有队列头,只不过等待队列头和普通的等待队列结点定义有所不同。

include/linux/wait.h
struct __wait_queue_head {
 spinlock_t lock;
 struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

可以看到,等待队列头结构中封装了list_head结构。这么做是可以想象到的,因为队列和栈本质上还是双联表。当我们适当限制双联表的某些操作时,就可以实现这样的功能。另外,等待队列头结构中还有一个自旋锁结构的变量lock,它起到了对等待队列进行互斥操作的作用。 在等待队列结构中,除了使用list_head结构外,还有以下几个字段:

typedef struct __wait_queue wait_queue_t;
struct __wait_queue {
unsigned int flags;
#define WQ_FLAG_EXCLUSIVE 0x01
void *private;
wait_queue_func_t func;
struct list_head task_list;
};

flag:指明该等待的进程是互斥还是非互斥,为0时非互斥,为1时互斥; WQ_FLAG_EXCLUSIVE :从变量可以看出,此宏代表进程是互斥的; private:void型指针变量功能强大,你可以赋给它你需要的结构体指针。一般赋值为task_struct类型的指针,也就是说指向一个进程; func:函数指针,指向该等待队列项的唤醒函数; 我们可以通过下述图来详细了解等待队列头和等待队列的结构关系: 5850668_1   1.定义及初始化 对于结构的初始化 内核中使用init_waitqueue_head宏来初始化等待队列头。

include/linux/wait.h
#define init_waitqueue_head(q)                          \
        do {                                            \
                static struct lock_class_key __key;     \
                                                        \
                __init_waitqueue_head((q), &__key);     \
        } while (0)

事实上,这个do-while循环语句只会执行一次。那么,为什么要选择使用这个循环语句?在定义宏的时候将上述语句嵌套在一个大括号里也可以啊!可能我们如下那样使用一个宏:

         if(conditon)
             init_waitqueue_head(q);
         else
             do_somthing_else();

如果我们去除do-while,那么替换后会编译错误。 在init_waitqueue_head宏中调用的__init_waitqueue_head函数定义如下:

void __init_waitqueue_head(wait_queue_head_t *q, struct lock_class_key *key)
{
        spin_lock_init(&q->lock);
        lockdep_set_class(&q->lock, key);
        INIT_LIST_HEAD(&q->task_list);
}

在这个函数中,首先利用自旋锁初始化函数初始化这个自旋锁;在上述等待队列头的定义中,我们可以看到task_list字段是一个list_head结构类型,因此我们使用INIT_LIST_HEAD对这个链表进行初始化。这些过程都是我们所熟悉的。 对于初始化的一些函数与宏,我们可以查看代码即可。包括添加/移除等待队列 例如:add_wait_queue添加函数将等待队列wait添加到以q为等待队列头的那个等待队列链表中。

22void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
23{
24        unsigned long flags;
25
26        wait->flags &= ~WQ_FLAG_EXCLUSIVE;
27        spin_lock_irqsave(&q->lock, flags);
28        __add_wait_queue(q, wait);
29        spin_unlock_irqrestore(&q->lock, flags);
30}

我们可以看到flags的结果必然是0,也就是说这个函数是将非互斥进程添加到等待队列当中。而且在调用具体的添加函数时候,使用关中断并保护现场的自旋锁方式使得添加操作每次只被一个进程访问。 具体的添加过程是将当前进程所对应的等待队列结点wait添加到等待队列头结点q之后。具体来说,就是将new->task_list结点添加到以head->task_list为头指针的双链表当中。另外,通过add_wait_queue_exclusive函数可以将一个互斥进程添加到等待队列当中。从添加过程可以发现,add_wait_queue函数会将非互斥进程添加到等待队列的前部。

122static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new)
123{
124        list_add(&new->task_list, &head->task_list);
125}

另外, add_wait_queue_exclusive添加函数则会将互斥进程添加到等待队列的末尾。

33void add_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
34{
35        unsigned long flags;
36
37        wait->flags |= WQ_FLAG_EXCLUSIVE;
38        spin_lock_irqsave(&q->lock, flags);
39        __add_wait_queue_tail(q, wait);
40        spin_unlock_irqrestore(&q->lock, flags);
41}

remove_wait_queue函数用于将等待队列项wait从以q为等待队列头的等待队列中移除,源码如下。

44void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
 45{
 46        unsigned long flags;
 47
 48        spin_lock_irqsave(&q->lock, flags);
 49        __remove_wait_queue(q, wait);
 50        spin_unlock_irqrestore(&q->lock, flags);
 51}

150static inline void __remove_wait_queue(wait_queue_head_t *head,
151                                                        wait_queue_t *old)
152{
153        list_del(&old->task_list);
154}

有了上述的基础,那么移除函数就简单了许多。

这里我们重点说一下在等待队列上睡眠

实现进程阻塞大致过程就是将当前进程的状态设置成睡眠状态,然后将这个进程加入到等待队列即可。在linux内核中有一组函数接口来实现这个功能。

4313void __sched interruptible_sleep_on(wait_queue_head_t *q)
4314{
4315        sleep_on_common(q, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
4316}
4317EXPORT_SYMBOL(interruptible_sleep_on);
4318
4319long __sched
4320interruptible_sleep_on_timeout(wait_queue_head_t *q, long timeout)
4321{
4322        return sleep_on_common(q, TASK_INTERRUPTIBLE, timeout);
4323}
4324EXPORT_SYMBOL(interruptible_sleep_on_timeout);
4325
4326void __sched sleep_on(wait_queue_head_t *q)
4327{
4328        sleep_on_common(q, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
4329}
4330EXPORT_SYMBOL(sleep_on);
4331
4332long __sched sleep_on_timeout(wait_queue_head_t *q, long timeout)
4333{
4334        return sleep_on_common(q, TASK_UNINTERRUPTIBLE, timeout);
4335}
4336EXPORT_SYMBOL(sleep_on_timeout);

通过上述源码,你可以发现这些函数在内部都调用了sleep_on_common函数,通过传递不同的值来实现不同的功能。而这个通用函数的三个参数分别关注的是:进程要加入到那个等待队列?进程是那种睡眠状态(TASK_UNINTERRUPTIBLE还是TASK_INTERRUPTIBLE)?进程睡眠的时间?

4292static long __sched
4293sleep_on_common(wait_queue_head_t *q, int state, long timeout)
4294{
4295        unsigned long flags;
4296        wait_queue_t wait;
4297
4298        init_waitqueue_entry(&wait, current);
4299
4300        __set_current_state(state);
4301
4302        spin_lock_irqsave(&q->lock, flags);
4303        __add_wait_queue(q, &wait);
4304        spin_unlock(&q->lock);
4305        timeout = schedule_timeout(timeout);
4306        spin_lock_irq(&q->lock);
4307        __remove_wait_queue(q, &wait);
4308        spin_unlock_irqrestore(&q->lock, flags);
4309
4310        return timeout;
4311}

在此函数中,首先定义了一个等待队列项结点,通过 init_waitqueue_entry函数对其进行初始化。可以从下述初始化源码中看到,此时该等待队列项指向当前当前进程。而唤醒函数指针func则指向内核自定义的一个默认唤醒函数default_wake_function。

 98static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
 99{
100        q->flags = 0;
101        q->private = p;
102        q->func = default_wake_function;
103}

初始化完毕后,通过__set_current_state函数将当前进程的状态设置成state。接着,在自旋锁的保护下,将当前进程对应的等待队列结点插入到等待队列链表当中。更重要的是,在schedule_timeout函数中不仅要设置进程的睡眠时间(以jiffies为单位的),还要使用schedule函数进行重新调度。一旦使用了schedule函数后,也就意味这当前这个进程真正的睡眠了,那么接下来的代码会在它唤醒后执行。当该进程被唤醒后(资源可用时),会从等待队列中将自己对应的那个等待队列结点wait移除。 上述过程都是在自旋锁保护下进行的,并且在整个执行过程中不可被其他中断所打断。现在再回过头去看一开始的那四个睡眠函数接口,你就明白了它们各自的不同之处了。

5.唤醒函数

唤醒函数会唤醒以x为头结点的等待队列中的等待队列项所对应的进程。与睡眠函数类似,内核中也有一组函数可以对阻塞的进程进行唤醒。

170#define wake_up(x)                      __wake_up(x, TASK_NORMAL, 1, NULL)
171#define wake_up_nr(x, nr)               __wake_up(x, TASK_NORMAL, nr, NULL)
172#define wake_up_all(x)                  __wake_up(x, TASK_NORMAL, 0, NULL)

175#define wake_up_interruptible(x)        __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
176#define wake_up_interruptible_nr(x, nr) __wake_up(x, TASK_INTERRUPTIBLE, nr, NULL)
177#define wake_up_interruptible_all(x)    __wake_up(x, TASK_INTERRUPTIBLE, 0, NULL)

通过上述代码,我们可以发现这些唤醒函数均调用了__wake_up函数。__wake_up函数的四个参数分别指:头结点指针、唤醒进程的类型、唤醒进程的数量和一个附加的void型指针变量。

3999void __wake_up(wait_queue_head_t *q, unsigned int mode,
4000                        int nr_exclusive, void *key)
4001{
4002        unsigned long flags;
4003
4004        spin_lock_irqsave(&q->lock, flags);
4005        __wake_up_common(q, mode, nr_exclusive, 0, key);
4006        spin_unlock_irqrestore(&q->lock, flags);
4007}

在__wake_up函数又通过传递不同的参数调用__wake_up_common函数来实现不同的唤醒功能。

3975static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
3976                        int nr_exclusive, int wake_flags, void *key)
3977{
3978        wait_queue_t *curr, *next;
3979
3980        list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
3981                unsigned flags = curr->flags;
3982
3983                if (curr->func(curr, mode, wake_flags, key) &&
3984                                (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
3985                        break;
3986        }
3987}

list_for_each_entry_safe函数将遍历整个等待队列中的链表,通过每次的逻辑判断来唤醒相应的进程。

这个if语句初看起来有点麻烦,不过我们一点一点的将它拆解。 curr->func(curr, mode, sync, key):即执行默认的唤醒函数,将指定的进程curr以mode方式唤醒。成功唤醒返回1;否则,返回0; (flags & WQ_FLAG_EXCLUSIVE):判断当前进程是否以互斥形式唤醒。

是互斥形式则返回1;否则返回0; !–nr_exclusive:nr_exclusive为需要唤醒的互斥进程的数量。 这三个部分是通过逻辑与连接起来的。根据逻辑与的运算规则,只有当第一部分为真时才会判断第二部分的值,依次再判断第三部分的值。 通过上述的等待队列的添加过程我们知道,

等待队列中前面均是非互斥进程,后面才是互斥进程。因此,唤醒函数总先唤醒全部的非互斥进程。因为当__wake_up_commom函数每一次去判断if语时,总会“不自觉”去执行默认的唤醒函数(除非唤醒失败,那么会退出遍历宏);当全部的非互斥进程被唤醒后,第二个判断条件也就成立了。因此__wake_up_commom函数会依次唤醒nr_exclusive个互斥进程;当–nr_exclusive为0时(!–nr_exclusive也就为真),整个遍历过程也恰好结束,而此时if语句的三个判断条件才刚好满足(这段代码太强大了!!!)。

关于条件睡眠,虽然函数实现与睡眠函数不同,但是基本思想是相似的,可以查找相应的源码进行分析。

进程状态&&父子信号分析

September 9th, 2014

接着http://www.lizhaozhong.info/archives/596写。

在操作系统原理里面,我们只学了进程有三个主要状态:阻塞态,就绪态,运行态。
在linux下,我们拓展出了 O,S,R,I,Z,T,B,D。我们可以使用top命令查看S位。

O:进程正在处理器运行,这个状态从来木见过.
S:休眠状态(sleeping)
R:等待运行(runable)R Running or runnable (on run queue) 进程处于运行或就绪状态
I:空闲状态(idle)
Z:僵尸状态(zombie)   
T:跟踪状态(Traced)
B:进程正在等待更多的内存页
D:不可中断的深度睡眠,一般由IO引起,同步IO在做读或写操作时,cpu不能做其它事情,只能等待,这时进程处于这种状态,如果程序采用异步IO,这种状态应该就很少见到了

» Read more: 进程状态&&父子信号分析