系统调用的执行过程

8 12 月, 2010 by edsionte 1 comment »

当用户态的进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。在X86体系中,可以通过两种不同的方式进入系统调用:执行int $0x80汇编命令和执行sysenter汇编命令。后者是Intel在PentiumII中引入的指令,内核从2.6版本开始支持这条命令。本文将集中讨论以int $0x80方式进入系统调用的过程。

通过int $0x80方式调用系统调用实际上是用户进程产生一个中断向量号为0x80的软中断。当用户态进程发出int $0x80指令时,CPU将从用户态切换到内核态并开始执行system_call()。这个函数是通过汇编命令来实现的,它是0x80号软中断对应的中断处理程序。对于所有系统调用来说,它们都必须先进入system_call(),也就是所谓的系统调用处理程序。再通过系统调用号跳转到具体的系统调用服务例程处。

在该函数执行之前,CPU控制单元已经将eflags、cs、eip、ss和esp寄存器的值自动保存到该进程对应的内核栈中。随之,在system_call内部首先将存储在eax寄存器中的系统调用号压入栈中。接着执行SAVE_ALL宏。该宏在栈中保存接下来的系统调用可能要用到的所有CPU寄存器。

1/linux/arch/i386/kernel/entry.S
2 241ENTRY(system_call)
3 242        pushl %eax                      # save orig_eax
4 243        SAVE_ALL

通过GET_THREAD_INFO宏获得当前进程的thread_inof结构的地址;再检测当前进程是否被其他进程所跟踪,也就是thread_inof结构中flag字段的_TIF_SYSCALL_TRACE或_TIF_SYSCALL_AUDIT被置1。如果发生被跟踪的情况则转向相应的处理命令处。

1244        GET_THREAD_INFO(%ebp)
2245        # system call tracing in operation
3246        testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
4247        jnz syscall_trace_entry

接着,对用户态进程传递过来的系统调用号的合法性进行检查。如果不合法则跳入到syscall_badsys标记的命令处。

1248        cmpl $(nr_syscalls), %eax
2249        jae syscall_badsys

如果系统调用好合法,则跳入相应系统调用号所对应的服务例程当中,也就是在sys_call_table表中找到了相应的函数入口点。由于sys_call_table表的表项占4字节,因此获得服务例程指针的具体方法是将由eax保存的系统调用号乘以4再与sys_call_table表的基址相加。

当系统调用服务例程结束时,从eax寄存器中获得当前进程的的返回值,并把这个返回值存放在曾保存用户态eax寄存器值的那个栈单元的位置上。这样,用户态进程就可以在eax寄存器中找到系统调用的返回码。

1250syscall_call:
2251        call *sys_call_table(,%eax,4)
3252        movl %eax,EAX(%esp)             # store the return value

至此,用户进程进入系统调用的过程大致分析完毕。

fork系统调用分析(1)-准备工作

7 12 月, 2010 by edsionte 无评论 »

接下来的几篇文章将会分析fork系统调用在内核中的实现,以作为本学期linux操作系统课程总的分析报告。本文所采用的源码均为v2.6.11。和最新内核相比,do_fork函数的变化较大,而核心函数copy_process则几乎保持不变。

操作系统需要一种机制用于创建新进程,fork()就是Linux或Unix提供给程序员用于创建进程的方法。fork函数的相关信息如下:

通常,我们程序中直接使用的fork函数是将fork系统调用封装之后而产生的。通过上面的表格,我们知道fork函数用于创建新的进程,所创建的进程称为当前进程的子进程,而当前进程也随之称为子进程的父进程。通常可以通过父子进程不同的返回值来区分父子进程,并使其执行不同功能的代码。

那么,用户态下的fork函数是如何调用fork系统调用的?内核中是如何创建子进程的?父子进程的返回值是如何产生的?这是本文所重点讨论的。

进程描述符

进程是操作系统中一个重要的基本概念。通常我们认为进程是程序的一次执行过程。为了描述和控制进程的运行,操作系统为每个进程定义了一个数据结构,即进程控制块(Process Control Block,PCB)。我们通常所说的进程实体包含程序段,数据段和PCB三部分,PCB在进程实体中占据重要的地位。所谓的创建进程,实质上就是创建PCB的过程;而撤销进程,实质上也就是对PCB的撤销。

上述内容是我们在操作系统原理课上所学习到的。在Linux内核中,PCB对应着一个具体的结构体——task_struct,也就是所谓的进程描述符(process descriptor)。该数据结构中包含了与一个进程相关的所有信息,比如包含众多描述进程属性的字段,以及指向其他与进程相关的结构体的指针。因此,进程描述符内部是比较复杂的。我们可以通过图1大致的了解进程描述符的结构。

图1(图片来自ULK)

可以看到,进程描述符中有指向mm_struct结构体的指针mm,这个结构体是对该进程用户空间的描述;也有指向fs_struct结构体的指针fs,这个结构体是对进程当前所在目录的描述;也有指向files_struct结构体的指针files,这个结构体是对该进程已打开的所有文件进行描述;另外还有一个小型的进程描述符(low-level information)——thread_info。在这个结构体中,也有指向该进程描述符的指针task。因此,这两个结构体是相互关联的。关于thread_info结构的详细说明可以参考本系列文章的后续分析。

与存储在磁盘上的程序相比,我们认为是进程是动态的。这是因为一个进程在其“一生”中可能处于不同的状态。因此,在进程描述符中,使用state字段对该进程当前所处的状态进行描述。在内核中,使用一组宏来描述进程可能所处的状态。这些宏之间是互斥的,也就是说进程一次最多只能使用一个宏。因为,进程在某一刻只可能处于一种状态。下面对本文中所涉及到的进程状态进行简单的描述:

可运行状态(TASK_RUNNING)
如果进程正在CPU上执行或者正在等待被调度程序所调度,那么它的状态就处于可运行状态。

暂停状态(TASK_STOPPED)
进程的执行被暂定,也就是我们常说的阻塞状态和等待状态。当进程接收到SIGSTOP、SIGSTP等信号时,就进入该状态。

跟踪状态(TASK_TRACED)
当一个进程被另一个进程跟踪监控时,这个进程就处于该状态。最常见的场景就是我们调试一个程序,被调试的程序就处于此状态。

fork系统调用

在用户态下,使用fork()创建一个进程对我们来说已经不再陌生。除了这个函数,一个新进程的诞生还可以分别通过vfork()和clone()。fork、vfork和clone三个API函数均由C库提供,它们分别在C库中封装了与其同名的系统调用fork(),vfork()和clone()。API所封装的系统调用对编程者是隐藏的,编程者只需知道如何使用这些API即可。

上述三个系统调用所对应的系统调用号在linux/include/asm-i386/unistd.h中定义如下:

1#define __NR_restart_syscall      0
2#define __NR_exit                 1
3#define __NR_fork                 2
4…… ……
5#define __NR_clone              120
6…… ……
7#define __NR_vfork              190
8…… ……

传统的创建一个新进程的方式是子进程拷贝父进程所有资源,这无疑使得进程的创建效率低,因为子进程需要拷贝父进程的整个地址空间。更糟糕的是,如果子进程创建后又立马去执行exec族函数,那么刚刚才从父进程那里拷贝的地址空间又要被清除以便装入新的进程映像。

为了解决这个问题,内核中提供了上述三种不同的系统调用。

1. 内核采用写时复制技术对传统的fork函数进行了下面的优化。即子进程创建后,父子以只读的方式共享父进程的资源(并不包括父进程的页表项)。当子进程需要修改进程地址空间的某一页时,才为子进程复制该页。采用这样的技术可以避免对父进程中某些数据不必要的复制。

2. 使用vfork函数创建的子进程会完全共享父进程的地址空间,甚至是父进程的页表项。父子进程任意一方对任何数据的修改使得另一方都可以感知到。为了使得双方不受这种影响,vfork函数创建了子进程后,父进程便被阻塞直至子进程调用了exec()或exit()。由于现在fork函数引入了写时复制技术,在不考虑复制父进程页表项的情况下,vfork函数几乎不会被使用。

3. clone函数创建子进程时灵活度比较大,因为它可以通过传递不同的参数来选择性的复制父进程的资源。具体参数可参见表1

就像一开始所分析的那样,用户程序并不直接使用系统调用,而是通过C库中的API。而系统调用在内核中也并不是直接实现的,而是通过调用各自对应的服务例程。系统调用fork、vfork和clone在内核中对应的服务例程分别为sys_fork(),sys_vfork()和sys_clone()。因此,想要了解fork等系统调用的详细执行过程,就必须查看它们所对应的内核函数(也就是服务例程)是如何实现的。上述三个系统调用对应的服务例程分别定义在linux/arch/i386/kernel/process.c 中,具体如下:

01asmlinkage int sys_fork(struct pt_regs regs)
02{
03    return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
04}
05 
06asmlinkage int sys_clone(struct pt_regs regs)
07{
08    unsigned long clone_flags;
09    unsigned long newsp;
10    int __user *parent_tidptr, *child_tidptr;
11 
12    clone_flags = regs.ebx;
13    newsp = regs.ecx;
14    parent_tidptr = (int __user *)regs.edx;
15    child_tidptr = (int __user *)regs.edi;
16    if (!newsp)
17        newsp = regs.esp;
18    return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr);
19}
20 
21asmlinkage int sys_vfork(struct pt_regs regs)
22{
23    return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
24}

可以看到do_fork()均被上述三个服务例程调用。而在do_fork()内部又调用了copy_process(),因此我们可以通过图2来理解上述的调用关系。

 

                                                                     图2
从上面的分析中,我们已经明确了do_fork()和copy_process()是本文的主要分析对象。但是在这之前,我们有必要先分析一下用户进程进入系统调用的过程。详见下文。

在内核中添加系统调用

1 12 月, 2010 by edsionte 2 comments »

如何往内核中添加自己写的系统调用?其实步骤非常简单:

1.编写一个系统调用;
2.在系统调用表末尾加入一个新表项;
3.在< asm/unistd.h >中添加一个新的系统调用号;
4.重新编译内核;

上述工作完成后,就可以在用户程序中使用自己所编写的系统调用了。接下来,我们将逐步分析如何上述步骤。

1.编写系统调用

我们将要实现一个获得当前进程pid的的系统调用。对于一个进程,我们可以直接通过current->pid来获得。为了使得这个系统调用同样适用于一个线程,我们使用current->tgid。这么做的原因是同一个线程组内的所有线程TGID均相同;而一个进程的PID和TGID是相同的。

1asmlinkage long sys_mygetpid(void)
2{
3    return current->tgid;
4}

与普通函数不同的是,这个系统调用的函数名前有asmlinkage修饰符。这个修饰符使得GCC编译器从堆栈中取该函数的参数而不是寄存器中。另外,系统调用函数的命名规则都是sys_XXX的形式。

接下来,我们的要做的是将这个函数放于何处。一种方法是,将上述函数放于/kernel/下的某个文件中;另一种方式是,将这个函数单独存放在/kernel/下的一个新建的.c文件中。不管何种方法,所做的目的都是为了在重新编译内核时将上述我们所编写的系统调用函数编译进内核。

2.在系统调用表中添加新的表项

linux中为每一个系统调用都分配一个系统调用号。也就是说,每一个系统调用号都唯一的对应着一个系统调用。内核中使用系统调用表来记录所有已经注册过的系统调用,这个系统调用表存储在sys_call_table中。在yoursource/arch/x86/kernel/syscall_table_32.S中可以看到系统系统调用表。

我们所要做的就是在该表的末尾添加我们刚编写的系统调用:.long sys_getpid。我们并不需要显式的指定系统调用号,从0开始算起,我们所编写的函数的系统调用号为341。

01ENTRY(sys_call_table)
02       .long sys_restart_syscall       /* 0 - old "setup()" system call, used for restarting */
03       .long sys_exit
04       .long ptregs_fork
05       .long sys_read
06       .long sys_write
07       .long sys_open          /* 5 */
08          …… ……
09      .long sys_perf_event_open
10      .long sys_recvmmsg
11      .long sys_fanotify_init
12      .long sys_fanotify_mark
13      .long sys_prlimit64             /* 340 */
14      .long sys_mygetpid

3.在< asm/unistd.h >中添加一个新的系统调用号

上一步,在系统调用表中添加新的表项是为了将系统调用号和系统调用关联起来。现在,我们需要在unistd.h文件中添加具体的系统调用号,这样使得系统可以根据这个系统调用号在系统调用表中查找具体的系统调用。

当用户进程调用一个系统调用时,其实是通过一个中断号为128的软中断来通知内核的。此时,内核会由用户态切换到内核态转而去执行这个软中断对应的中断处理程序。而这个中断处理程序恰好就是系统调用处理程序。也就是说,任何系统调用都会引发CPU去执行这个系统调用处理程序。因此,必须通过系统调用号来识别不同的系统调用。系统调用号通常会存储在eax寄存器中。

现在我们就在yoursource/arch/x86/include/asm/unistd_32.h文件中添加新的系统调用号。在上一步我们所添加的sys_mygetpid系统调用对应的编号为341,因此我们在该文件的末尾添加下面的语句:#define __NR_mygetpid 341。注意这里的宏命名规则,以__NR_开头。

1#define __NR_restart_syscall      0
2#define __NR_exit                 1
3#define __NR_fork                 2
4#define __NR_read                 3
5#define __NR_write                4
6       …………
7#define __NR_fanotify_mark      339
8#define __NR_prlimit64          340
9#define __NR_mygetpid         341

4.编译内核

如果上述三个步骤都完成后,那么接下来重新编译内核即可。具体可参见这里

5.编写用户态的程序

1#include < linux/unistd.h >
2 
3_syscall0(int,mygetpid)
4 
5int main()
6{
7    printf("The current process's pid is %d\n",mygetpid());
8    return 0;
9}

上述用户程序可能与我们平日里所写的稍有不同。主要区别是增加了_syscall0(int,mygetpid)这个宏。因为我们现在直接在程序中调用系统调用,而我们平时则是通过调用C库中的API来间接调用系统调用。在unistd.h文件中有_syscallN()宏的定义,这里的N可取0~6。N代表的是需要传递给系统调用的参数个数。由于mygetpid系统调用需传递的参数个数为0,因此选取_syscall0。另外,这组宏的内部参数分布有如下特点:第一个参数是系统调用返回值的类型,第二个参数是系统调用函数的名称,接下来的参数按照系统调用参数的次序依次是参数类型和参数名称。对于每个宏来说,都有2+2*N个参数。

OK,上述方法即可以将我们自己编写的系统调用函数加入到内核。try!

系统调用的那些事儿

30 11 月, 2010 by edsionte 无评论 »

update: 2011/09/21

1.系统调用

我们知道,Linux将整个虚拟地址空间划分为两部分:用户空间和内核空间。并且规定,用户空间不能直接访问内核空间,而内核空间则可以访问用户空间。通过这样的级别划分,可以使得内核空间更加的稳定和安全。但是,当用户进程必须访问内核或使用某个内核函数时,就得使用系统调用(System Call)。在Linux中,系统调用是用户空间访问内核空间的唯一途径。

系统调用是内核提供的一组函数接口,它使得用户空间上运行的进程可以和内核之间进行交互。比如,用户进程通过系统调用访问硬件设备或操作系统的某些资源等。系统调用如同内核空间和用户空间的一个传话者。内核如同一个高高在上的帝王,而用户空间的进程则属于级别很小的官员。由于用户进程资质太浅,当它需要得到内核的支持时,它并没有权利直接上报内核,而只能通过系统调用这个传话人来得到内核的支持。

具体的,用户程序通过应用编程接口来使用系统调用,而系统调用则是在内核中通过内核函数来实现的。

2.应用编程接口

应用编程接口(Application Programming Interface,API)其实就是程序员在用户空间下可以直接使用的函数接口。每个API会对应一定的功能。比如strlen(),它所实现的功能就是求所传递字符串的长度。

有时候,某些API所提供的功能会涉及到与内核空间进行交互。那么,这类API内部会封装系统调用。而不涉及与内核进行交互的API则不会封装系统调用。也就是说,API和系统调用并没有严格对应关系,一个API可能恰好只对应一个系统调用,比如read()API和read()系统调用;一个API也可能由多个系统调用实现;有时候,一个API的功能可能并不需要内核提供的服务,那么此时这个API也就不需要任何的系统调用,比如abs()。另外,一个系统调用可能还被多个不同的API内部调用。

对于编程者来说,系统调用和API都是一组函数,并无什么两样;但是事实上,系统调用的实现是在内核完成的,API则是在函数库中实现的。

API是用户程序直接可以使用的函数接口,但如果每个操作系统都拥有只属于自己的API,那么应用程序的移植性将会很差。基于POSIX(Portable Operating System Interface)标准的API拥有很好的可移植性,它定义了一套POSIX兼容标准,这使得按这个标准实现的API可以在各种版本的UNIX中使用。现如今,它也可以在除UNIX之外的操作系统中使用,比如Linux,Windows NT等。

3.函数库

一个.c文件会经过预处理、编译、汇编、链接四个步骤。在汇编阶段,输出的是.o文件,即我们常说的目标文件。目标文件并不能直接执行,它需要链接器的再一次加工。链接器将所有的目标文件集合在一起,加上库文件,最后才能得到可执行文件。

函数库完成了各种API函数的定义,只不过函数库是二进制的形式,我们不能直接去查看这些API函数如何实现。这些API函数的声明则散步在不同的头文件中,比如我们常用(也许你并未感知我们频繁的使用这个函数库)的标准函数库libc.so,在其中包含多个我们常用的函数定义,但是这些函数的声明却分布在stdio.h和string.h等头文件中。

我们每次在链接程序时,都必须告诉链接器需要链接到那个库中。只不过通常默认的链接让我们忽视了这一点。

比如,一个简单的helloworld程序中,仅使用了stdio.h头文件。我们当然可以这样轻松的编译:gcc helloworld.c -o helloworld。之所以可以毫无顾忌是因为stdio.h中所声明的函数都定义在libc.so中,而对于这个函数库,连接器是默然链接的。

如果我们编译如下程序:

01#include < stdio.h >
02#include < math.h >
03int main()
04{
05    double i;
06 
07    scanf("%lf",&i);
08    printf("%lf",sqrt(i));
09    return 0;
10}

按照我们以往的编译方法显然是不行的:

1edsionte@edsionte-desktop:~$ gcc test.o -o test
2test.o: In function `main':
3test.c:(.text+0x39): undefined reference to `sqrt'
4collect2: ld returned 1 exit status

因为在这个程序中使用了math.h头文件,而这个头文件中声明的函数sqrt()被定义在libm.so函数库中。那么,这个时候应该这样编译:gcc test.c -o test -lm。最后的-lm选项即告诉链接器需要加入libm.so函数库。

上述一步到位的编译方法似乎又无形中掩盖了函数库的加入时间。如果我们按照编译程序的四个步骤依次处理相应文件时,就可以发现只有到了最后的链接过程中才会出现上述错误信息。也就是说,函数库的加入是在链接部分。

从上述内容中,我们知道应用程序直接使用的并不是系统调用(不过可以通过_syscallN的方法直接使用系统调用)而是API。内核中提供的每个系统调用都通过libc库封装成相应的API。如果一个API函数中包含系统调用,那么它通常在libc库中会对应一个封装例程(wrapper routine)。封装例程可能正好对应一个与API同名的系统调用,有时为了实现更加复杂的功能会封装多个系统调用。

4.系统命令

每一个系统命令其实就是一个可执行的程序,这些可执行程序的实现调用了某些系统调用。并且,这些可执行程序又分为普通用户可使用的命令和管理员可使用的命令。根据上述分类,普通用户可用的命令和管理可用的命令分别被存放于/bin和/sbin目录下。

5.系统调用的服务例程

系统调用的实现是在内核中完成的,它通过封装对应的内核函数(通常是以sys_开头,再加上相应的系统调用名)来实现其代表的功能。内核函数和用户空间中函数并无两样,只不过内核函数是在内核中实现。也就是说,用户程序通过某个系统调用进入内核后,会接着去执行这个系统调用对应的内核函数。这个内核函数也称为系统调用的服务例程

由于内核函数是在内核中实现的,因此它必须符合内核编程的规则,比如函数名以sys_开始,函数定义时候需加asmlinkage标识符等。

边学边实践:打印VFS中的结构体

27 11 月, 2010 by edsionte 无评论 »

学习了VFS的基本原理,我们很有必要对这些理论知识进行验证和实践。本文所分析的几个小程序将更具体、直观的展现VFS中一些数据结构之间的逻辑关系。

1.打印超级块和索引结点

通过前面的分析我们知道,系统中所有的超级块都内嵌一个struct list_head类型的字段s_list。通过该字段将系统中所有的超级块链接成一个双联表。因此,如果我们知道这个双联表的头指针以及了解相关便利宏的使用方法,那么我们就可以遍历整个系统中的所有超级块了。
为了解释方便,我们将内嵌的list_head结构体称为内部结构体;将super_block结构体称为外部结构体;

具体的,我们可以通过 list_for_each宏来遍历一个list_head类型的双联表。该宏的定义如下:

1364#define list_for_each(pos, head) \
2365        for (pos = (head)->next; prefetch(pos->next), pos != (head); \
3366                pos = pos->next)

使用该宏时,需要向head参数中传递要遍历双联表的头指针;而每次遍历得到的list_head类型的结点地址会保存在pos这个参数中。不过,这个宏只能遍历内嵌于超级块中的那个list_head结构的链表,并不能得到正在被遍历的那个超级块的地址(也就是指向当前正被遍历的超级块的指针)。也就是说,每次遍历时并不能得到超级块中的其他字段。因此,还应该使用 list_entry宏。该宏通过指向list_head结点的地址来得到外部超级块的首地址。

1345#define list_entry(ptr, type, member) \
2346        container_of(ptr, type, member)

这个宏的第一个参数是指向内部结构体list_head的指针,第二个参数是外部结构体的类型,第三个参数是list_head类型的变量在外部结构体中的名称。这个宏最后会返回指向当前外部结构体的指针。比如,在super_block结构体中,list_head结构类型的字段名称为s_list,因此可以如下使用该宏:

1sb = list_entry(pos, struct super_block, s_list);

对于超级块形成的双联表来说,它的头指针是super_blocks。但是很遗憾,super_blocks这个变量并没有被导出。所谓导出,就是通过EXPORT_SYMBOL将某个函数或者变量对全部内核代码公开。也就是说,使用 EXPORT_SYMBOL可以将一个函数以符号的方式导出给其他模块使用 。为了解决这个问题,我们可以在包含super_blocks的这个文件中将这个变量导出,并且重新编译内核。对于我们这里的这个小程序而言,这样做有些不值得。幸好,在/proc/kallsyms文件中,记录了内核中所有符号以及符号的地址。因此,在该文件中查找相应符号就可以得到其地址。

我们使用下述两条命令:

1edsionte@edsionte-desktop:~/code/vfs/print_sb$ grep super_blocks /proc/kallsyms
2c0772a30 D super_blocks
3edsionte@edsionte-desktop:~/code/vfs/print_sb$ grep " sb_lock" /proc/kallsyms
4c08c9d60 B sb_lock

就可以得到super_blocks变量的地址。另外,sb_lock超级块对应的自旋锁。

上述都准备好后,我们就可以进行遍历了。关键代码如下:

01#define SUPER_BLOCKS_ADDRESS 0xc0772a30
02#define SB_LOCK_ADDRESS 0xc08c9d60
03 
04static int __init my_init(void)
05{
06struct super_block *sb;
07struct list_head *pos;
08struct list_head *linode;
09struct inode *pinode;
10unsigned long long count = 0;
11 
12printk("print some fields of super blocks:\n");
13spin_lock((spinlock_t *)SB_LOCK_ADDRESS);
14list_for_each(pos, (struct list_head *)SUPER_BLOCKS_ADDRESS){
15 
16sb = list_entry(pos, struct super_block, s_list);
17printk("dev_t:%d,%d ",MAJOR(sb->s_dev),MINOR(sb->s_dev));
18printk("fs_name:%s\n",sb->s_type->name);
19printk("\n");
20}
21 
22spin_unlock((spinlock_t *)SB_LOCK_ADDRESS);
23printk("the number of inodes:%llu\n",sizeof(struct inode)*count);
24 
25return 0;
26}

另外,需要注意的是,每次重启电脑后,都要重新查找上述两个变量的地址。

对于一个超级块中所有的inode,有专门一个链表将所有的inode链接起来。这个链表的头结点是超级块中的s_inode字段。而inode之间是其内部的i_sb_list字段进行链接的。了解了这些,我们可以在上述程序的基础上,再打印每个超级块中的所有inode:

01list_for_each(pos, (struct list_head *)SUPER_BLOCKS_ADDRESS){
02 
03    sb = list_entry(pos, struct super_block, s_list);
04    printk("dev_t:%d,%d ",MAJOR(sb->s_dev),MINOR(sb->s_dev));
05    printk("fs_name:%s\n",sb->s_type->name);
06 
07    list_for_each(linode, &sb->s_inodes){
08 
09        pinode = list_entry(linode, struct inode, i_sb_list);
10        count ++;
11        printk("%lu\t",pinode->i_ino);
12    }
13 
14    printk("\n");
15}

在上面代码的基础上,我们再加深一步。一个索引结点可能对应若干个dentry,这些dentry自身通过其内部的d_alias链接在一起;而整个链表的头结点是inode中的i_dentry字段。因此,根据上面的方法,我们可以在遍历每个inode的同时,继续遍历这个inode对应的所有dentry。部分代码如下:

01list_for_each(pos, (struct list_head *)SUPER_BLOCKS_ADDRESS){
02    sb = list_entry(pos, struct super_block, s_list);
03    printk("dev_t:%d,%d ",MAJOR(sb->s_dev),MINOR(sb->s_dev));
04    printk("fs_name:%s\n",sb->s_type->name);
05 
06    list_for_each(linode, &sb->s_inodes){
07        pinode = list_entry(linode, struct inode, i_sb_list);
08        count ++;
09        printk("%lu[",pinode->i_ino);
10 
11        list_for_each(ldentry, &pinode->i_dentry){
12            pdentry = list_entry(ldentry, struct dentry, d_alias);
13            printk("%s->",pdentry->d_name.name);
14        }
15        printk("]\t");
16    }
17     
18    printk("\n");
19}

上出程序的完整的代码在这里

2.打印文件类型结构体

同样的道理,通过下述的代码可以打印file_system_type结构体。

01#define FILE_SYSTEM_ADDRESS 0xc08ca3a4 /* grep file_systems /proc/kallsyms */
02#define FILE_SYSTEM_LOCK_ADDRESS 0xc0772de0 /* grep file_systems_lock /proc/kallsyms */
03 
04static int __init printfs_init(void)
05{
06    struct file_system_type **pos;
07 
08    printk("\n\nprint file_system_type:\n");
09 
10    read_lock((rwlock_t *)FILE_SYSTEM_LOCK_ADDRESS);
11    pos = (struct file_system_type **)FILE_SYSTEM_ADDRESS;
12 
13    while(*pos){
14        printk("name: %s\n",(*pos)->name);
15        pos = &((*pos)->next);
16    }
17 
18    read_unlock((rwlock_t *)FILE_SYSTEM_LOCK_ADDRESS);
19 
20    return 0;
21}

更多的打印信息可以按照上述方法继续添加。开始吧!

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key