日志标签 ‘源码分析’

open()在Linux内核的实现(2)-路径查找

2015年2月10日

1.基本说明

文件的打开操作在内核中的实现思路很简单:即通过用户态传递的路径逐项查找文件;如果该文件存在,那么内核将为该文件创建file结构;同时将该file结构与files数组关联,最终返回数组的索引作为用户态的文件描述符。

路径查找是对给定的文件路径以目录项为单位进行逐级解析。主要包括以下几项内容:

1.确定路径查找的起始位置。比如,起始位置可能是current->fs->cwd或current->fs->root;

2.当前进程是否有对目录项关联的inode进行访问的权限;

3.根据当前的目录项,对下一级目录项进行查找;这里的查找可能是向下查找子文件,也可能是向上反查父目录(比如下一级目录项为“..”);

4.处理挂载点问题;当前目录项如果是挂载点,那么必须处理不同文件系统之间的跨越;

5.处理符号链接文件;如果当前目录项为一个符号链接文件,那么必须追随(follow)该文件所指向的真实文件;

6.查找并创建文件路径中所缺失的部分;比如,通过open()创建一个新文件时,那么所传递的路径中可能有部分目录项当前是不存在的;

其中,第1项是路径查找的首要工作;2~6项是在路径查找过程中,针对每个目录项进行检查确认的。

负责open系统调用基本实现的是do_sys_open(),其内部所调用的do_filp_open函数承担了大部分open的实现过程,其中就包括路径查找。

2.函数分析

2.1.do_filp_open

open操作的核心函数为do_filp_open,它解析文件路径并新建file结构。该函数内部创建nd变量,传入并调用了path_openat()。nameidata类型的nd在整个路径查找过程中充当中间变量,它既可以为当前查找输入数据,又可以保存本次查找的结果。

struct file *do_filp_open(int dfd, const char *pathname,
		const struct open_flags *op, int flags)
{
	struct nameidata nd;
	struct file *filp;

	filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
	if (unlikely(filp == ERR_PTR(-ECHILD)))
		filp = path_openat(dfd, pathname, &nd, op, flags);
	if (unlikely(filp == ERR_PTR(-ESTALE)))
		filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
	return filp;
}

在这个函数中,path_openat有可能会被调用三次。通常内核为了提高效率,会首先在RCU模式(rcu-walk)下进行文件打开操作;如果在此方式下打开失败,则进入普通模式(ref-walk)。第三次调用比较少用,目前只有在nfs文件系统才有可能会被使用。接下来将主要说明前两种调用方式。

2.2.path_openat

path_openat()其函数声明如下:

static struct file *path_openat(int dfd, const char *pathname,
		struct nameidata *nd, const struct open_flags *op, int flags);

该函数描述了整个路径查找过程的基本步骤,这里做简单说明。每个具体步骤的实现过程,将在本文以及后续文章中做详析说明。

1.首先通过get_empty_flip()分配一个新的file结构,分配前会对当前进程的权限和文件最大数进行判断;

2.path_init()对接下来的路径遍历做一些准备工作,主要用于判断路径遍历的起始位置,即通过根目录/,或当前路径(pwd),或指定路径(openat系统调用可以指定);

3.将当前进程的total_link_count置为0;

3.link_path_walk()对所打开文件路径进行逐一解析,每个目录项的解析结果都存在nd参数中;

4.根据最后一个目录项的结果,do_last()将填充filp所指向的file结构;

5.如果上一步中的filp所指为空,将说明当前文件为符号链接文件;

6.如果设置了LOOKUP_FOLLOW标志,则通过follow_link()进入符号链接文件所指文件,填充file;否则,直接返回当前符号链接文件的filp;

7.最终返回file结构;

2.3.path_init

path_init()用于设置路径搜寻的起始位置,主要体现在设置nd变量。其函数声明如下:

static struct file *path_openat(int dfd, const char *pathname,
		struct nameidata *nd, const struct open_flags *op, int flags);

如果flags设置了LOOKUP_ROOT标志,则表示该函数被open_by_handle_at函数调用,该函数将指定一个路径作为根;这属于特殊情况,这里暂不分析;接下来path_init主要分三种情况设置nd。

1.如果路径名name以/为起始,则表示当前路径是一个绝对路径,通过set_root设置nd;否则,表示路径name是一个相对路径;

2.如果dfd为AT_FDCWD,那么表示这个相对路径是以当前路径pwd作为起始的,因此通过pwd设置nd;

3.如果dfd不是AT_FDCWD,表示这个相对路径是用户设置的,需要通过dfd获取具体相对路径信息,进而设置nd;

上述步骤2和3都表示要打开的文件路径是以相对路径为起始的,但是两者稍有不同。步骤2为我们通常默认的open操作,而步骤3具体指的是openat系统调用,这一点体现在不同打开系统调用向do_sys_open中dfd参数所传递的值。

不管上述哪一种打开情况,均要设置nd变量,它是一个nameidata类型。在path_init中,nd的last_type都被默认设置成了LAST_ROOT。

在path_init中,如果为上述步骤1,则通过当前进程的fs->root字段更新nd的root字段,并且nd的path字段也指向root字段;如果为步骤2,则通当前进程fs->pwd更新nd的path字段;如果为步骤3,则先通过文件描述符dfd获取用户指定的工作目录file结构,然后通过file的f_path字段更新nd的path字段。需要注意的,步骤2和步骤3均未设置root字段。最终,nd中的inode字段均由path.dentry->d_inode更新。

2.4.link_path_walk

link_path_walk()主要用于对各目录项逐级遍历。其函数声明如下:

static int link_path_walk(const char *name, struct nameidata *nd);

该函数核心部分是通过一个循环完成的。在进入这个循环之前,如果路径name是一个绝对路径,那么该函数还对路径进行了一些处理,即过滤掉绝对路径/前多余的符号/。

在循环中,所要做的工作包含如下:

1.next为path类型的变量,指向下一个目录项;name指向被搜索的路径;this为qstr类型变量,表示当前搜索路径所处目录项的哈希值,用type指明当前目录项类型;

2.如果有必要,为当前目录项更新哈希值,并保存在this中;

3.如果当前目录项为“.”,则type为LAST_DOT;如果目录项为“..”,则type为LAST_DOTDOT;否则,type默认为LAST_NORM;

4.如果当前目录项紧邻的分隔符/有多个(比如/home///edsionte),则将其过滤,即使name指向最后一个/;

5.通过walk_component()处理当前目录项,更新nd和next;如果当前目录项为符号链接文件,则只更新next;

6.如果当前目录项为符号链接文件,则通过nested_symlink()进行处理,更新nd;

7.如果name中的目录项遍历完毕,则结束;否则进行下一轮循环;

通过上述循环,将用户所指定的路径name从头至尾进行了搜索,至此nd保存了最后一个目录项的信息,但是内核并没有确定最后一个目录项是否真的存在,这些工作将在do_last()中进行。

2.5.walk_component

walk_component()位于link_path_walk函数之中。该函数声明如下:

static inline int walk_component(struct nameidata *nd, struct path *path,
		struct qstr *name, int type, int follow)

在每次循环中,它将获取当前目录项的dentry结构以及inode结构等信息,即更新nd。如果当前目录项对应的inode不存在,那么将向用户态返回ENOENT;在该函数中,定义了变量inode,它将保存当前目录项对应的索引节点。

根据当前目录项类型的不同,对目录项的处理流程也不同。该函数的具体流程如下:

1.如果type为LAST_DOT和LAST_DOTDOT,将进入handle_dots()对当前目录项进行“walk”;

2.如果当前目录项为普通目录项,则通过do_lookup()对其进行处理;

3.如果should_follow_link()获知当前目录项为符号链接文件,则退出当前函数。具体的,如果当前walk模式为rcu,则直接返回-ECHILD,否则返回1。返回-ECHILD时候,将直接返回到do_filp_open(),进行ref-walk模式重新查找;如果返回1,则返回至上层函数link_path_walk(),进入netsted_symlink()进行符号链接目录项的处理;

也就是说,一旦当前目录项为符号链接文件,则需要通过ref-walk进行处理。这是因为处理符号链接文件需要通过具体文件的处理函数进行实现,这个过程可能会导致阻塞,这与rcu方式是违背的,因此需要先转换到ref-walk;

4.至此,如果当前目录项查找成功,则通过path_to_nameidata()更新nd;

3.总结

本文重点说明了open实现过程中的路径查找过程。open中的路径查找是针对用户所传递路径,按照目录项逐级进行遍历查找;对于路径中的每个目录项,不同类型的目录项有不同的处理方法。如果需要了解对“.”、“..”以及符号连接文件的处理方法,可以阅读本系列后续文章。

参考资料:

1.Linux源码3.2.69;

2.Linux系统调用open七日游:http://blog.chinaunix.net/uid-20522771-id-4419666.html

3.深入理解Linux内核:http://book.douban.com/subject/2287506/;

4.深入Linux内核架构:http://book.douban.com/subject/4843567/;

5.Linux内核探秘:http://book.douban.com/subject/25817503/;

open()在Linux内核的实现-准备工作

2015年1月4日

1.基本说明

“open()在Linux内核的实现”系列文章将分析open系统调用在Linux内核中的实现过程。本系列文章分为六篇,每篇文章都描述 open()实现的一部分内容,与前后的系列文章保持相对独立。本文属于前序文章,集中说明后续文章涉及到的基本原理和基本数据结构,并且对整个分析过程进行Q&A。

本系列文章参考Linux内核源码版本为3.2.69。

2.数据结构

dentry结构

对于打开文件这个操作来说,它是通过路径名查找对应文件inode的过程,这里用户直面的是文件路径,而内核关注的inode。在文件路径和inode之间通过目录项(dentry)缓存进行关联,dentry缓存加快了vfs对文件的查找。所有的目录项通过散列表进行组织,这样可以快速对dentry进行查找;此外,内核将常用的dentry通过LRU算法进行组织,这样可以快速查到最近一段时间经常使用的dentry。

下面将对dentry中的部分字段进行说明。

d_inode:该字段指向目录项所关联的文件。如果该字段为空,则说明当前目录项指向的是一个并不存在的文件。

d_name:该字段表示目录项名称(并不是整个路径名),但它并不是单纯的字符串,而是将字符串文件名、字符串长度和散列值封装成qstr(quick string)结构,这样可以加速目录项的查找工作;

d_iname:当目录项名称长度小于DNAME_INLINE_LEN时,则该字符串名称则直接通过该字段进行存储;

d_parent:一个路径中的目录项形成层级结构。该字段指向当前目录项的父目录dentry实例;特别的,对于根目录项来说,这个字段指向自己;

d_subdirs:当前目录项如果代表目录,则该目录下的所有文件对应的dentry将形成d_subdirs链表(表头);

d_child:这个字段是父目录dentry中d_subdirs链表中的结点;

d_alias:一个文件可能有多个名称(硬链接),即多个dentry,则一个文件的所有目录项则形成一个链表,这个链表头位于该文件inode中的i_dentry字段,d_alias充当的该链表中的结点;

vfsmount结构

每个挂载在内核目录树中的文件系统都将对应一个vfsmount结构,下面将对该结构中的部分字段进行说明。假设设备/dev/sdc为ntfs文件系统,现需要将其挂载在文件系统为ext3的/home/edsionte/work下。因此,/home/edsionte/work可以被称为ntfs文件系统的挂载点,并且称ntfs文件系统与ext3文件系统形成父子文件文件系统关系。同时ntfs也可称为源文件系统,而ext3也可称为目的文件系统。

mnt_hash:内核将系统内所有已挂载的文件系统通过散列表的形式进行组织,每个vfsmount将处于其对应哈希值的冲突链表当中。mnt_hash字段则为具体冲突链表的元素。

mnt_mounts:如果当前文件系统下挂载了其他的子文件系统,那么这些子文件系统将通过自身vfsmount中的mnt_child字段组成一个链表,该链表头为父文件系统中的mnt_mounts字段。

mnt_child:当前文件系统将通过该字段与其他父文件系统下的子文件系统组成一个链表。

mnt_parent:该字段指向父文件系统对应的vfsmount结构。即指向ext3文件系统对应的vfsmount结构。

mnt_mountpoint:该字段表示源文件系统在目的文件系统中挂载点对应的dentry结构。/home/edsionte/work为挂载点,则该字段指向目录项work。

mnt_root:指向当前文件系统的根目录项。对于源文件系统ntfs来说,根目录项相对为/,但在整个系统目录树中,根目录项为work。

mnt_sb:每个文件系统都将对应一个super_block结构,该字段指向/dev/sdc设备上文件系统对应的超级块。

mnt_list:所有处于一个名字空间的文件系统通过mnt_list字段链接在一起,而该链表的表头为该名字空间结构中的list字段。

mnt_ns:该字段表示当前vfsmount所对应的名字空间结构。

nameidata结构

文件路径是由各级别的目录项组成的,因此路径的查找过程是对目录项的逐级查找。nameidata结构是路径查找过程中的核心数据结构,在每一级目录项查找过程中,它向查找函数输入参数,并且保存本次查找的结果,因此它是不断变化的。

下面对nameidata结构中的部分字段进行说明。

path:该字段用于保存当前目录项。该字段是path结构,该结构将目录项和该目录项所关联的vfsmount结构进行封装。

last:该字段为qstr结构,表示当前目录项的名称。

root:该字段为path结构,表示根目录。

last_type:表示当前目录项的类型。

inode:表示当前目录项对应的inode,它的取值来自于path.dentry.d_inode。

depth:表示符号链接当前的嵌套级别,最大不能超过MAX_NESTED_LINKS;

saved_names:该字符串数组表示符号链接每个嵌套级别的名称;

目录项的类型包括以下几种情况:

LAST_NORM:普通目录项;

LAST_ROOT:当前目录项为/;

LAST_DOT:当前目录项为.;

LAST_DOTDOT:当前目录项为..;

LAST_BIND:当前目录项为符号链接文件;

3.基本原理

rcu机制

写时拷贝(rcu,Read-Copy-Update)是Linux内核的一种锁机制,它是一种改良的rwlock(但并不能代替),适合读者多写者少的情景,可以保证读写者操作同时进行。

对于读者而言,rcu机制可以保证多个读者在不申请锁的情况下直接对临界区资源进行访问。对于写者而言,它之所以可以与读者同时访问共享资源,是因为在读者读取原始数据的同时它修改的是原始数据的备份。当所有读者都退出访问该共享资源时,写着将用修改后的新数据替换原始数据。同时,rcu中的回收机制将对原始数据进行回收。

与rwlock相比,在读多写少的情况下,rcu的效率会高很多。因为rcu所提供的拷贝技术使读写者可以同时访问共享资源,因此免去了读写者申请锁时所花费的开销。

由于rcu机制的自身特点,它所使用的上下文必须是不可睡眠的。因为,写者在替换原始数据之前会等待所有读者退出临界区,而此时如果读者处于阻塞状态,那么系统将进入死锁状态。

rcu-walk和ref-walk

内核中的路径查找提供两种模式:ref-walk和rcu-walk。前者是内核中传统的路径查找方式,而ref-walk是基于rcu所机制的一种路径查找模式。由于路径查找正好是一个读多写少的情景,基于rcu机制快速高效的特点,该模式可以高效的进行路径查找。不过,rcu-walk并不是万能的,如果路径查找过程中需要睡眠,那么必须将查找模式由rcu-walk切换到ref-walk。

4.总结

本篇对open()在内核实现中所涉及的数据结构和原理进行实现说明,并且针对open()实现过程的一些问题进行Q&A。可以在阅读open()内核源码之前阅读本文,也可在阅读之后再次阅读本文。

参考资料:

1.Linux源码3.2.69;

2.深入理解Linux内核:http://book.douban.com/subject/2287506/;

3.深入Linux内核架构:http://book.douban.com/subject/4843567/;

4.Linux内核探秘:http://book.douban.com/subject/25817503/;

register_chrdev_region函数源码分析

2010年9月21日

如何找到一个有效的切入点去深入分析内核源码,这是一个令人深思的问题。本文以前文中未详细说明的函数为切入点,深入分析char_dev.c文件的代码。如果你已经拥有了C语言基础和一些数据结构基础,那么还等什么?Let’s go!

在《字符设备驱动分析》一文中,我们说到register_chrdev_region函数的功能是在已知起始设备号的情况下去申请一组连续的设备号。不过大部分驱动书籍都没有去深入说明此函数,可能是因为这个函数内部封装了__register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name)函数的原因。不过我们不用苦恼,这正好促使我们去分析这个函数。

194int register_chrdev_region(dev_t from, unsigned count, const char *name)
 195{
 196        struct char_device_struct *cd;
 197        dev_t to = from + count;
 198        dev_t n, next;
 199
 200        for (n = from; n <\ to; n = next) {
 201                next = MKDEV(MAJOR(n)+1, 0);
 202                if (next >\ to)
 203                        next = to;
 204                cd = __register_chrdev_region(MAJOR(n), MINOR(n),
 205                               next - n, name);
 206                if (IS_ERR(cd))
 207                        goto fail;
 208        }
 209        return 0;
 210fail:
 211        to = n;
 212        for (n = from; n <\ to; n = next) {
 213                next = MKDEV(MAJOR(n)+1, 0);
 214                kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
 215        }
 216        return PTR_ERR(cd);
 217}

首先值得我们注意的是,这个函数每次分配的是一组设备编号。其中from参数是这组连续设备号的起始设备号,count是这组设备号的大小(也是次设备号的个数),name参数处理本组设备的驱动名称。另外,当次设备号数目过多(count过多)的时候,次设备号可能会溢出到下一个主设备。因此我们在for语句中可以看到,首先得到下一个主设备号(其实也是一个设备号,只不过此时的次设备号为0)并存储于next中。然后判断在from的基础上再追加count个设备是否已经溢出到下一个主设备号。如果没有溢出(next小于to),那么整个for语句就只执行个一次__register_chrdev_region函数;否则当设备号溢出时,会把当前溢出的设备号范围划分为几个小范围,分别调用__register_chrdev_region函数。

如果在某个小范围调用__register_chrdev_region时出现了失败,那么会将此前分配的设备号都释放。

其实register_chrdev_region函数还没有完全说清除设备号分配的具体过程,因为具体某个小范围的设备号是由__register_chrdev_region函数来完成的。可能你已经注意到在register_chrdev_region函数源码中出现了struct char_device_struct结构,我们首先来看这个结构体:

  50static struct char_device_struct {
  51        struct char_device_struct *next;
  52        unsigned int major;
  53        unsigned int baseminor;
  54        int minorct;
  55        char name[64];
  56        struct cdev *cdev;              /* will die */
  57} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

在register_chrdev_region函数中,在每个字符设备号的小范围上调用__register_chrdev_region函数,都会返回一个struct char_device_struct类型的指针。因此我们可以得知,struct char_device_struct类型对应的并不是每一个字符设备,而是具有连续设备号的一组字符设备。从这个结构体内部的字段也可以看出,这组连续的设备号的主设备号为major,次设备号起始为baseminor,次设备号范围为minorct,这组设备号对应的设备驱动名称为name,cdev为指向这个字符设备驱动的指针。

这里要特别说明的是,内核中所有已分配的字符设备编号都记录在一个名为chrdevs散列表里。该散列表中的每一个元素是一个 char_device_struct结构,这个散列表的大小为255(CHRDEV_MAJOR_HASH_SIZE),这是因为系统屏蔽了12位主设备号的前四位。既然说到散列表,那么肯定会出现冲突现象,因此next字段就是冲突链表中的下一个元素的指针。

接下来我们详细来析__register_chrdev_region函数。首先为cd变量分配内存并用零来填充(这就是用kzalloc而不是kmalloc的原因)。接着通过P操作使得后续要执行的语句均处于临界区。

  92static struct char_device_struct *
  93__register_chrdev_region(unsigned int major, unsigned int baseminor,
  94                           int minorct, const char *name)
  95{
  96        struct char_device_struct *cd, **cp;
  97        int ret = 0;
  98        int i;
  99
 100        cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
 101        if (cd == NULL)
 102                return ERR_PTR(-ENOMEM);
 103
 104        mutex_lock(&chrdevs_lock);

如果major为0,也就是未指定一个具体的主设备号,需要动态分配。那么接下来的if语句就在整个散列表中为这组设备寻找合适的位置,即从散列表的末尾开始寻找chrdevs[i]为空的情况。若找到后,那么i不仅代表这组设备的主设备号,也代表其在散列表中的关键字。当然,如果主设备号实现已指定,那么可不去理会这部分代码。

 105
 106        /* temporary */
 107        if (major == 0) {
 108                for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
 109                        if (chrdevs[i] == NULL)
 110                                break;
 111                }
 112
 113                if (i == 0) {
 114                        ret = -EBUSY;
 115                        goto out;
 116                }
 117                major = i;
 118                ret = major;
 119        }

接着对将参数中的值依次赋给cd变量的对应字段。当主设备号非零,即事先已知的话,那么还要通过major_to_index函数对其进行除模255运算,因此整个散列表关键字的范围是0~254。

 120
 121        cd->major = major;
 122        cd->baseminor = baseminor;
 123        cd->minorct = minorct;
 124        strlcpy(cd->name, name, sizeof(cd->name));
 125
 126        i = major_to_index(major);

至此,我们通过上面的代码会得到一个有效的主设备号(如果可以继续执行下面代码的话),那么接下来还不能继续分配。正如你所知的那样,散列表中的冲突是在所难免的。因此我们得到major的值后,我们要去遍历冲突链表,为当前我们所述的char_device_struct类型的变量cd去寻找正确的位置。更重要的是,我们要检查当前的次设备号范围,即baseminor~baseminor+minorct,是否和之前的已分配的次设备号(前提是major相同)范围有重叠。

下面的for循环就是在冲突链表中查找何时的位置,当出现以下三种情况时,for语句会停止。

(1)如果冲突表中正被遍历的结点的主设备号(*(cp)->major)大于我们所分配的主设备号(major),那么就可以跳出for语句,不再继续查找。此时应该说设备号分配成功了,那么cd结点只需等待被插到冲突链表当中(*cp节点之前)。

(2)如果(*cp)结点和cd结点的主设备号相同,但是前者的次设备号起点比cd结点的大,那么跳出for语句,等待下一步的范围重叠的检测。

(3)如果(*cp)结点和cd结点的主设备号相同,但是cd结点的次设备号起点小于(*cp)结点的次设备号的终点,那么会跳出for语句。此时很可能两个范围的次设备号发生了重叠。

由上面的分析可以看出,冲突表中是按照设备号递增的顺序排列的。

 127
 128        for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
 129                if ((*cp)->major > major ||
 130                    ((*cp)->major == major &&
 131                     (((*cp)->baseminor >= baseminor) ||
 132                      ((*cp)->baseminor + (*cp)->minorct > baseminor))))
 133                        break;

接下来检测当主设备号相同时,次设备范围是否发生了重叠。首先依次计算出新老次设备号的范围,接着进行范围判断。第一个判断语句是检测新范围的终点是否在老范围的之间;第二个判断语句是检测新范围的起点是否在老范围之间。

 134
 135        /* Check for overlapping minor ranges.  */
 136        if (*cp && (*cp)->major == major) {
 137                int old_min = (*cp)->baseminor;
 138                int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
 139                int new_min = baseminor;
 140                int new_max = baseminor + minorct - 1;
 141
 142                /* New driver overlaps from the left.  */
 143                if (new_max >= old_min && new_max <= old_max) {
 144                        ret = -EBUSY;
 145                        goto out;
 146                }
 147
 148                /* New driver overlaps from the right.  */
 149                if (new_min <= old_max && new_min >= old_min) {
 150                        ret = -EBUSY;
 151                        goto out;
 152                }
 153        }

当一切都正常后,就将char_device_struct描述符插入到中途链表中。至此,一次小范围的设备号分配成功。并且此时离开临界区,进行V操作。如果上述过程中有任何失败,则会跳转到out处,返回错误信息。

 154
 155        cd->next = *cp;
 156        *cp = cd;
 157        mutex_unlock(&chrdevs_lock);
 158        return cd;
 159out:
 160        mutex_unlock(&chrdevs_lock);
 161        kfree(cd);
 162        return ERR_PTR(ret);
 163}

至此,我们已经分析完了字符设备号分配函数。

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