存档在 2010年11月

系统调用的那些事儿

2010年11月30日

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中,而对于这个函数库,连接器是默然链接的。

如果我们编译如下程序:

#include < stdio.h >
#include < math.h >
int main()
{
	double i;

	scanf("%lf",&i);
	printf("%lf",sqrt(i));
	return 0;
}

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

edsionte@edsionte-desktop:~$ gcc test.o -o test
test.o: In function `main':
test.c:(.text+0x39): undefined reference to `sqrt'
collect2: 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中的结构体

2010年11月27日

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

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

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

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

 364#define list_for_each(pos, head) \
 365        for (pos = (head)->next; prefetch(pos->next), pos != (head); \
 366                pos = pos->next)

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

345#define list_entry(ptr, type, member) \
346        container_of(ptr, type, member)

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

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

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

我们使用下述两条命令:

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

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

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

#define SUPER_BLOCKS_ADDRESS 0xc0772a30
#define SB_LOCK_ADDRESS 0xc08c9d60

static int __init my_init(void)
{
struct super_block *sb;
struct list_head *pos;
struct list_head *linode;
struct inode *pinode;
unsigned long long count = 0;

printk("print some fields of super blocks:\n");
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 ",MAJOR(sb->s_dev),MINOR(sb->s_dev));
printk("fs_name:%s\n",sb->s_type->name);
printk("\n");
}

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

return 0;
}

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

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

	list_for_each(pos, (struct list_head *)SUPER_BLOCKS_ADDRESS){

		sb = list_entry(pos, struct super_block, s_list);
		printk("dev_t:%d,%d ",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\t",pinode->i_ino);
		}

		printk("\n");
	}

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

	list_for_each(pos, (struct list_head *)SUPER_BLOCKS_ADDRESS){
		sb = list_entry(pos, struct super_block, s_list);
		printk("dev_t:%d,%d ",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);

			list_for_each(ldentry, &pinode->i_dentry){
				pdentry = list_entry(ldentry, struct dentry, d_alias);
				printk("%s->",pdentry->d_name.name);
			}
			printk("]\t");
		}
		
		printk("\n");
	}

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

2.打印文件类型结构体

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

#define FILE_SYSTEM_ADDRESS 0xc08ca3a4 /* grep file_systems /proc/kallsyms */
#define FILE_SYSTEM_LOCK_ADDRESS 0xc0772de0 /* grep file_systems_lock /proc/kallsyms */

static int __init printfs_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;
}

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

VFS中的其他数据结构

2010年11月23日

Last Update:2010/11/27

本文涉及VFS中数据结构有:struct file_system_type;struct vfsmount;struct fs_struct;struct files_struct;struct nameidata;struct qstr;

与文件系统相关的数据结构

VFS中使用file_system_type结构来描述一个具体的文件系统类型。也就是说,Linux支持的所有文件系统类型都分别唯一的对应一个file_system_type结构。如果某个具体文件类型的文件系统被安装到了系统中,那么系统就会创建一个vfsmount结构。这个结构用来描述一种文件系统类型的一个安装实例。

struct file_system_type

该结构位于yoursource/include/linux/fs.h中,下面对该结构中部分字段进行说明:

1736struct file_system_type {
1737        const char *name;
1738        int fs_flags;
1739        int (*get_sb) (struct file_system_type *, int,
1740                       const char *, void *, struct vfsmount *);
1741        void (*kill_sb) (struct super_block *);
1742        struct module *owner;
1743        struct file_system_type * next;
1744        struct list_head fs_supers;
1745
1746        struct lock_class_key s_lock_key;
1747        struct lock_class_key s_umount_key;
1748        struct lock_class_key s_vfs_rename_key;
1749
1750        struct lock_class_key i_lock_key;
1751        struct lock_class_key i_mutex_key;
1752        struct lock_class_key i_mutex_dir_key;
1753        struct lock_class_key i_alloc_sem_key;
1754};

name:文件系统的名字,不能为空;
get_sb:在安装文件系统时,调用此指针所指函数以在磁盘中获取超级块;
kill_sb:卸载文件文件系统时候,调用此指针所指函数以进行一些清理工作;
owner:如果一个文件系统以模块的形式加载到内核,则该字段用来说明哪个模块拥有这个结构。一般为THIS_MODULE;
next:所有的文件系统类型结构形成一个链表,该链表的头指针为全局变量file_systems(struct file_system_type *file_systems)。这个字段指向链表中下一个文件系统类型结构;
fs_supers:同一个文件系统类型下的所有超级块形成一个双联表,这个字段是这个双联表的头结点。超级块之间通过s_instances字段相互链接;

struct vfsmount

在解释这个结构的相关字段之前,我们很有必要了解一些概念。当我们安装linux时,硬盘上已经有了一个分区安装了ext3(或ext4)文件系统,这个文件系统被称之为根文件系统。系统安装完毕后,如果要继续安装其他文件系统,就需要挂载(mount)。具体的,将要安装的文件系统的根目录挂载到根文件系统的某个子目录上。这样,新安装的文件系统的根目录就是父文件系统下的某个子目录,我们将这个子目录称为安装点(mount point)。

该结构存储在yoursource/include/linux/mount.h中,下面对该结构的部分字段进行说明:

  49struct vfsmount {
  50        struct list_head mnt_hash;
  51        struct vfsmount *mnt_parent;    /* fs we are mounted on */
  52        struct dentry *mnt_mountpoint;  /* dentry of mountpoint */
  53        struct dentry *mnt_root;        /* root of the mounted tree */
  54        struct super_block *mnt_sb;     /* pointer to superblock */
  55        struct list_head mnt_mounts;    /* list of children, anchored here */
  56        struct list_head mnt_child;     /* and going through their mnt_child */
  57        int mnt_flags;
  63        const char *mnt_devname;        /* Name of device e.g. /dev/dsk/hda1 */
  64        struct list_head mnt_list;
…… ……
  71        int mnt_id;                     /* mount identifier */
  72        int mnt_group_id;               /* peer group identifier */
  78        atomic_t mnt_count;
  79        int mnt_expiry_mark;            /* true if marked for expiry */
  80        int mnt_pinned;
  81        int mnt_ghosts;
…… ……
  87};

mnt_hash:内核通过哈希表对vfsmount进行管理,当前vfsmount结构通过该字段链入相应哈希值对应的链表当中;
mnt_parent:指向父文件系统对应的vfsmount结构;
mnt_mountpoint:指向该文件系统安装点对应的dentry;
mnt_root:该文件系统对应的设备根目录的dentry;(两者是否指向同一个dentry?mnt_root是指向该文件系统设备根目录的dentry,具体这个dentry是什么?)

mnt_sb:指向该文件系统对应的超级块;
mnt_child:同一个父文件系统中的所有子文件系统通过该字段链接成双联表;
mnt_mounts:该字段是上述子文件系统形成的链表的头结点;
mnt_list:所有已安装的文件系统的vfsmount结构通过该字段链接在一起;

与路径查找有关的辅助结构

我们在使用open系统调用时,给该函数的第一个参数传递的是文件路径名。open函数对文件的操作最终会转化为对该文件inode的操作。VFS为了识别目标文件,会沿着路径逐层查找。因此,VFS中引入了nameidata结构。nameidata结构用于在路径查找过程中记录中间信息和查找结果。该结构体定义在yoursource/include/linux/namei.h中。

  18struct nameidata {
  19        struct path     path;
  20        struct qstr     last;
  21        struct path     root;
  22        unsigned int    flags;
  23        int             last_type;
  24        unsigned        depth;
  25        char *saved_names[MAX_NESTED_LINKS + 1];
  26
  27        /* Intent data */
  28        union {
  29                struct open_intent open;
  30        } intent;
  31};
33struct qstr {
34        unsigned int hash;
35        unsigned int len;
36        const unsigned char *name;
37};

与进程有关的数据结构

struct fs_struct

每个进程都有自己的根目录和当前的工作目录,内核使用struct fs_struct来记录这些信息,进程描述符中的fs字段便是指向该进程的fs_struct结构。该结构定义于yoursource/include/linux/fs_struct.h结构中。

   6struct fs_struct {
   7        int users;
   8        spinlock_t lock;
   9        int umask;
  10        int in_exec;
  11        struct path root, pwd;
  12};

users:用户数量;
lock:保护该结构的自旋锁;
umask:打开文件时设置的文件访问权限;
paroot:进程的根目录;
pwd:进程的当前工作目录;

struct files_struct

一个进程可能打开多个文件。所有被某个进程已打开的文件使用struct files_struct来记录。进程描述符的files字段便指向该进程的files_struct结构。该结构定义于yoursource/include/linux/fdtable.h中。

  44struct files_struct {
  48        atomic_t count;
  49        struct fdtable *fdt;
  50        struct fdtable fdtab;
  54        spinlock_t file_lock ____cacheline_aligned_in_smp;
  55        int next_fd;
  56        struct embedded_fd_set close_on_exec_init;
  57        struct embedded_fd_set open_fds_init;
  58        struct file * fd_array[NR_OPEN_DEFAULT];
  59};

count:引用计数;
fdtab:内核专门使用fdtable结构(该结构也称为文件描述符表)来描述文件描述符。该字段为初始的文件描述符表;
fdt:指向fdtab描述符表;
next_fd:最近关闭的文件描述符中数值最小的下一个可用的文件描述符;
close_on_exec_init:执行exec()时需要关闭的文件描述符;
open_fds_init:当前已经打开的文件描述符;
fd_array[NR_OPEN_DEFAULT]:文件对象的初始化数组;

我们都知道open函数正确执行后会返回一个整形的文件描述符,其实这个整数便是fd_arrary数组的下标。我们所说的标准输入文件、标准输出文件和标准错误文件分别是这个数组的0、1和2号元素。

通常在x86体系架构中NR_OPEN_DEFAULT的大小为32,难道一个进程最多只可以打开32个文件吗?当然不是,我们在上述的字段描述中,对fdtab和fd_array都用“初始化”来修饰。也就是说,当一个进程打开的文件数目超过32的时候,内核就分配一个更大的文件对象指针数组(fd_array中的文件对象指针也会被“复制”新的数组中),以便可以存放更多打开文件所对应的file结构体的指针。

上述字段已经说过,内核通过专门的struct fdtable来描述文件描述符表。该结构定义在yoursource/include/linux/file.h中。

  32struct fdtable {
  33        unsigned int max_fds;
  34        struct file ** fd;      /* current fd array */
  35        fd_set *close_on_exec;
  36        fd_set *open_fds;
  37        struct rcu_head rcu;
  38        struct fdtable *next;
  39};

max_fds:当前文件对象数组中元素的个数;
fd:指向当前文件对象数组,初始指向fd_arrary;
close_on_exec:执行exec时要关闭的文件描述符,初始指向close_on_exec_init;
open_fds:当前已经打开的文件描述符,初始指向open_fds_init;

VFS中的基本关系图

2010年11月21日

分析了VFS中几个基本的数据结构后,根据自己的理解,我画了一个关系图。如下(点击看大图):

关于此图的说明:

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

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

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

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

Linux内存管理实践-查找内存区域

2010年11月20日

查找内存区域

该模块实现的功能是:通过向内核模块中传递一个虚存地址,进而查找该地址所在的内存区域。具体查找的功能我们通过find_vma函数来实现,这个函数的原型如下:

struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr);

该函数在指定的地址空间中搜索第一个vm_end大于addr的内存区域。也就是说,当找到了第一个包含该addr或vm_start大于addr的内存区域时,该函数就返回相应区域的vm_area_struct结构体指针,否则返回NULL。那么,find_vma这个函数有可能找到的vma并不是包含addr的内存区域。这是怎么一回事?还是先看图吧。

在图(b)中,所找到的VMA的vm_end大于addr,但是addr却刚好是处于两个VMA之间。因此,find_vma函数所找到的VMA并不包含addr。正因为如此,在find_myvma函数中在使用完find_vma函数后,还要进测addr是否真正包含在vma中。具体函数实现可参考下述代码。本模块完整代码在这里

static ulong address = 0;
static void find_myvma(void)
{
	struct mm_struct *mm = current->mm;
	struct vm_area_struct *vma;

	printk("find the vma..\n");

	down_read(&mm->mmap_sem);
	vma=find_vma(mm, address);
	/* when the vma_start > address, it means that the address doesn't belong to the vma */
	if(vma && address <= vma->vm_start)
	{
		printk("address 0x%lx found in the VMA:0x%lx-0x%lx ",address,vma->vm_start,vma->vm_end);
		if(vma->vm_flags & VM_READ)
			printk("r");
		else
			printk("-");

		if(vma->vm_flags & VM_WRITE)
			printk("w");
		else
			printk("-");

		if(vma->vm_flags & VM_EXEC)
			printk("x");
		else
			printk("-");

		if(vma->vm_flags & VM_SHARED)
			printk("s");
		else
			printk("p");
		printk("\n");
	}
	else
	{
		printk("address 0x%lx didn't find in the VMA which you just now found..\n",address);
	}
	up_read(&mm->mmap_sem);
}

module_param(address, ulong, S_IRUGO);

这个内核模块涉及到了内核模块传递参数的问题。既然我们需要查找某个线性地址addr所在的vma,因此就需要在每次加载内核模块时传入addr这个参数。

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