IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> linux内核容器数据结构链表list.h -> 正文阅读

[数据结构与算法]linux内核容器数据结构链表list.h

最近想看看linux设备驱动开发类的书籍。记录一下,方便以后查阅。

一个驱动程序管理很多个设备,为了在驱动程序中跟踪每个设备,一般来说都是用到链表。链表有两种类型:单链表和双链表。

内核开发者要保持最少的代码,只实现了循环双向链表,因为这个结构能够实现FIFO和LIFO。为了使用内核实现的链表,需要加入头文件<linux/list.h>,其中最核心的数据结构是struct list_head,其定义如下:

struct list_head {
    struct list_head *next,*prev;
};

这里主要说明一下平时我们看到的循环双向链表和内核实现的循环双向链表的区别。

我们在上学时学到的循环双向链表一般都是这样实现:

struct car {
    int number;
    char *color;
    struct car *prev;
    struct car *next; 
};

如下图所示:

这样的实现方式是结构体的成员加入指向自己本身的指针。

具体的建立链表,添加节点,删除节点,遍历链表的实现这里就不写了。?

但是这样的实现方式也有一个缺点,就是当我们有各种不同类别的对象时,需要为每一种对象的链表写一个链表的增删改查的操作。那么有没有一种统一个方式呢?当然有:

既然传统实现链表的方法不能包含所有类别的对象,那么我们可以让不同类别的对象包含同一种“链表节点”!

解释一下刚才说的那句话,也就是内核链表的实现方法,我们这样定义car结构体:

struct car {
    int number;
    char *color;
    struct list_head list;
};

其中 struct list_head就是本文最开始提到的结构。

struct list_head {
    struct list_head *next,*prev;
};

这样实现的方式跟传统方式的区别主要在于结构体里面的指针类型。传统方法里面的指针是指向car这个结构本身:struct car *prev, *next;。而现在方法里面包含一个list_head结构,list_head结构里面的指针是指向list_head这个结构的,而不是指向整个car结构。

如下图所示:

看到这里相信有人会有点疑惑:这样实现循环链表的方式只是list_head互相指来指去。我们访问不了整个car结构里面的数据部分啊,比如car里面的number和color,即使获得了当前节点的指针,但是这个指针不是指向car结构体的,访问不了结构体的数据域啊。别着急,请往下看。

linux内核函数定义了一个宏container_of,具体定义在include/linux/kernel.h中。这个宏的主要作用:根据一个结构体的某个成员地址,名称和结构体类型,可以得到指向这个结构体的指针。这一点很容易想通,整个结构体所有成员的内存大小都知道了,根据某一个成员的地址,计算一下偏移量,就可以获得整个结构体的首地址。

定义如下:

#define container_of(ptr, type, member) ({                      \
	 const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
	(type *)( (char *)__mptr - offsetof(type,member) );})

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

解释一下:?

container_of(ptr, type, member)
包含的参数如下:
ptr:指向结构体某成员的指针(结构体某成员的地址)
type:整个结构体的类型
member: ptr所指向结构体成员的名字

看到这里,刚才的那个疑惑就可以解释了。虽然循环链表的实现方式是结构体里面的list_head在指来指去,当我们需要访问结构体里面的数据域时就可以根据car这个结构体成员list_head的地址找到整个结构体的地址,进而对结构体内的数据域进行访问。

看到这里应该就豁然开朗了:接下来看一下链表的建立,初始化,增加节点,删除节点,和遍历链表吧。

1.初始化和建立链表:

有两种方法创建和初始化链表:

/* 方法1:*/

struct list_head carlist;
INIT_LIST_HEAD(&carlist);

//宏定义如下:
static inline void INIT_LIST_HEAD(struct list_head *list)
{
	list->next = list;
	list->prev = list;
}

/* 方法2:*/

LIST_HEAD(carlist);

//宏定义如下:
#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)
#define LIST_HEAD_INIT(name) { &(name), &(name) }

?这两种方法都是都是建立了struct list_head 类型的变量,也就是循环链表的头节点,这个节点是不包含数据域部分的。跟car这个结构体里面的数据部分无关。

2.创建和添加链表节点

?主要有两个接口:

?????????头插法list_add(),这种方法可以实现堆栈。

? ? ? ? ?尾插法lsit_add_tail(),这种方法可以实现队列。

尾插法和头插法都调用同一个内部函数_list_add,头插法好理解,直接就在head和head->next之间插入节点即可。尾插法其实也一样,因为是循环双向链表,头节点的前一个节点就是尾节点,所以就在head->prev,head之间插入节点即可。

/* 头插法 */
static inline void list_add(struct list_head *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}

/* 尾插法 */
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
	__list_add(new, head->prev, head);
}


/* _list_add 将头节点,头节点的下一个节点,待插入的节点作为参数,
   跟普通的链表实现一样 */
static inline void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next)
{
	next->prev = new;
	new->next = next;
	new->prev = prev;
	prev->next = new;
}

以carlist为例:

/* 头插法 */

/* 创建链表节点 */
struct car *redcar = (struct car *)malloc(sizeof(struct car));
struct car *bluecar = (struct car *)malloc(sizeof(struct car));

/* 初始化节点 */
INIT_LIST_HEAD(&bluecar->list);
INIT_LIST_HEAD(&redcar->list);

/* 填充字段 */
redcar->color = "red";
redcar->number = 1;

bluecar->color = "blue";
bluecar->door_number = 2;

/* 加入链表中 */
list_add(&redcar->list, &carlist);
list_add(&bluecar->list, &carlist);

3.删除链表节点

?接口:list_del()

static inline void list_del(struct list_head *entry)
{
	__list_del(entry->prev, entry->next);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;
	prev->next = next;
}

删除后,为什么还要设置删除掉的节点指针呢?因为删除后,该选节点已不在链表当中,因此不会再使用。LIST_POISON1/2是两个不会存在于内核空间的地址,如果使用,就会报错?。

#define LIST_POISON1  ((void *) 0x00100100)
#define LIST_POISON2  ((void *) 0x00200200)

注意,list_del只是断开了节点的prev和next的指针,分配给当前节点的内存还需要手动释放。?

以carlist为例,删除红色的车:

list_del(&redcar->list);
free(redcar);

4.链表的遍历

接口:list_for_each_entry

我们可以看到使用了container_of宏,以car结构为例,即根据car结构体的成员struct list_head list的地址获取整个结构体的地址。

/**
 * list_for_each_entry	-	遍历链表
 * @pos:	用于迭代,就像是for(i=0,i<foo,i++)一样
 * @head:	链表的头节点指针
 * @member:	list_struct这个结构的名称,我们的例子中是list
 */
#define list_for_each_entry(pos, head, member)				\
	for (pos = list_entry((head)->next, typeof(*pos), member);	\
	     &pos->member != (head); 	\
	     pos = list_entry(pos->member.next, typeof(*pos), member))


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

以carlist为例:

struct car *acar;
    
list_for_each_entry(acar, &carlist, list)
{
    printf("the car's color:%s\n",acar->color);
}

5.整合一下整个过程:

#include "list.h"
#include <stdio.h>
#include <stdlib.h>

struct car
{
    int number;
    char *color;
    struct list_head list;
};

int main()
{
    /* 建立链表,头节点 */
    static LIST_HEAD(carlist);    
    
    /* 创建小车实例1,并插入链表 */
    struct car *redcar = (struct car *)malloc(sizeof(struct car));
    INIT_LIST_HEAD(&redcar->list);  /* 初始化所创建的节点 */
    redcar->color = "red";
    redcar->number = 1;
    list_add(&redcar->list, &carlist);

    /* 创建小车实例2,并插入链表 */
    struct car *bluecar = (struct car *)malloc(sizeof(struct car));
    INIT_LIST_HEAD(&bluecar->list);
    bluecar->color = "blue";
    bluecar->number = 2; 
    list_add(&bluecar->list, &carlist);
    
    /* 创建小车实例3,并插入链表 */
    struct car *yellowcar = (struct car *)malloc(sizeof(struct car));
    INIT_LIST_HEAD(&yellowcar->list);
    yellowcar->color = "yellow";
    yellowcar->number = 3; 
    list_add(&yellowcar->list, &carlist);
    
    /* 遍历链表 */
    struct car *acar;
    list_for_each_entry(acar, &carlist, list)
    {
        printf("the car color:%s,number:%d\n",acar->color,acar->number);
    }
    
    /* 删除一个元素 */
    list_del(&redcar->list);
    free(redcar);    
    
    printf("\nwe delete red car\n");

    /* 再次遍历链表 */
    list_for_each_entry(acar, &carlist, list)
    {
        printf("the car color:%s,number:%d\n",acar->color,acar->number);
    }
    
}

附上list.h的代码,这个list.h是整合了一下linux里面list.h的链表函数。在使用时,只需要加入list.h即可。

/*
 * Copyright (C) 2012 Fusion-io.  All rights reserved.
 *
 * This header was taken from the Linux kernel
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU General Public
 *  License v2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#ifndef _LINUX_LIST_H
#define _LINUX_LIST_H

#define LIST_POISON1  ((void *) 0x00100100)
#define LIST_POISON2  ((void *) 0x00200200)

#undef offsetof
#ifdef __compiler_offsetof
#define offsetof(TYPE,MEMBER) __compiler_offsetof(TYPE,MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif

#define container_of(ptr, type, member) ({                      \
	 const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
	(type *)( (char *)__mptr - offsetof(type,member) );})

/*
 * Simple doubly linked list implementation.
 *
 * Some of the internal functions ("__xxx") are useful when
 * manipulating whole lists rather than single entries, as
 * sometimes we already know the next/prev entries and we can
 * generate better code by using them directly rather than
 * using the generic single-entry routines.
 */

struct list_head {
	struct list_head *next, *prev;
};

#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)

static inline void INIT_LIST_HEAD(struct list_head *list)
{
	list->next = list;
	list->prev = list;
}

/*
 * Insert a new entry between two known consecutive entries.
 *
 * This is only for internal list manipulation where we know
 * the prev/next entries already!
 */
#ifndef CONFIG_DEBUG_LIST
static inline void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next)
{
	next->prev = new;
	new->next = next;
	new->prev = prev;
	prev->next = new;
}
#else
extern void __list_add(struct list_head *new,
			      struct list_head *prev,
			      struct list_head *next);
#endif

/**
 * list_add - add a new entry
 * @new: new entry to be added
 * @head: list head to add it after
 *
 * Insert a new entry after the specified head.
 * This is good for implementing stacks.
 */
#ifndef CONFIG_DEBUG_LIST
static inline void list_add(struct list_head *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}
#else
extern void list_add(struct list_head *new, struct list_head *head);
#endif


/**
 * list_add_tail - add a new entry
 * @new: new entry to be added
 * @head: list head to add it before
 *
 * Insert a new entry before the specified head.
 * This is useful for implementing queues.
 */
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
	__list_add(new, head->prev, head);
}

/*
 * Delete a list entry by making the prev/next entries
 * point to each other.
 *
 * This is only for internal list manipulation where we know
 * the prev/next entries already!
 */
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;
	prev->next = next;
}

/**
 * list_del - deletes entry from list.
 * @entry: the element to delete from the list.
 * Note: list_empty on entry does not return true after this, the entry is
 * in an undefined state.
 */
#ifndef CONFIG_DEBUG_LIST
static inline void list_del(struct list_head *entry)
{
	__list_del(entry->prev, entry->next);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}
#else
extern void list_del(struct list_head *entry);
#endif

/**
 * list_replace - replace old entry by new one
 * @old : the element to be replaced
 * @new : the new element to insert
 * Note: if 'old' was empty, it will be overwritten.
 */
static inline void list_replace(struct list_head *old,
				struct list_head *new)
{
	new->next = old->next;
	new->next->prev = new;
	new->prev = old->prev;
	new->prev->next = new;
}

static inline void list_replace_init(struct list_head *old,
					struct list_head *new)
{
	list_replace(old, new);
	INIT_LIST_HEAD(old);
}
/**
 * list_del_init - deletes entry from list and reinitialize it.
 * @entry: the element to delete from the list.
 */
static inline void list_del_init(struct list_head *entry)
{
	__list_del(entry->prev, entry->next);
	INIT_LIST_HEAD(entry);
}

/**
 * list_move - delete from one list and add as another's head
 * @list: the entry to move
 * @head: the head that will precede our entry
 */
static inline void list_move(struct list_head *list, struct list_head *head)
{
        __list_del(list->prev, list->next);
        list_add(list, head);
}

/**
 * list_move_tail - delete from one list and add as another's tail
 * @list: the entry to move
 * @head: the head that will follow our entry
 */
static inline void list_move_tail(struct list_head *list,
				  struct list_head *head)
{
        __list_del(list->prev, list->next);
        list_add_tail(list, head);
}

/**
 * list_is_last - tests whether @list is the last entry in list @head
 * @list: the entry to test
 * @head: the head of the list
 */
static inline int list_is_last(const struct list_head *list,
				const struct list_head *head)
{
	return list->next == head;
}

/**
 * list_empty - tests whether a list is empty
 * @head: the list to test.
 */
static inline int list_empty(const struct list_head *head)
{
	return head->next == head;
}

/**
 * list_empty_careful - tests whether a list is empty and not being modified
 * @head: the list to test
 *
 * Description:
 * tests whether a list is empty _and_ checks that no other CPU might be
 * in the process of modifying either member (next or prev)
 *
 * NOTE: using list_empty_careful() without synchronization
 * can only be safe if the only activity that can happen
 * to the list entry is list_del_init(). Eg. it cannot be used
 * if another CPU could re-list_add() it.
 */
static inline int list_empty_careful(const struct list_head *head)
{
	struct list_head *next = head->next;
	return (next == head) && (next == head->prev);
}

static inline void __list_splice(struct list_head *list,
				 struct list_head *head)
{
	struct list_head *first = list->next;
	struct list_head *last = list->prev;
	struct list_head *at = head->next;

	first->prev = head;
	head->next = first;

	last->next = at;
	at->prev = last;
}

/**
 * list_splice - join two lists
 * @list: the new list to add.
 * @head: the place to add it in the first list.
 */
static inline void list_splice(struct list_head *list, struct list_head *head)
{
	if (!list_empty(list))
		__list_splice(list, head);
}

/**
 * list_splice_init - join two lists and reinitialise the emptied list.
 * @list: the new list to add.
 * @head: the place to add it in the first list.
 *
 * The list at @list is reinitialised
 */
static inline void list_splice_init(struct list_head *list,
				    struct list_head *head)
{
	if (!list_empty(list)) {
		__list_splice(list, head);
		INIT_LIST_HEAD(list);
	}
}

/**
 * list_entry - get the struct for this entry
 * @ptr:	the &struct list_head pointer.
 * @type:	the type of the struct this is embedded in.
 * @member:	the name of the list_struct within the struct.
 */
#define list_entry(ptr, type, member) \
	container_of(ptr, type, member)

/**
 * list_for_each	-	iterate over a list
 * @pos:	the &struct list_head to use as a loop cursor.
 * @head:	the head for your list.
 */
#define list_for_each(pos, head) \
	for (pos = (head)->next; pos != (head); \
        	pos = pos->next)

/**
 * __list_for_each	-	iterate over a list
 * @pos:	the &struct list_head to use as a loop cursor.
 * @head:	the head for your list.
 *
 * This variant differs from list_for_each() in that it's the
 * simplest possible list iteration code, no prefetching is done.
 * Use this for code that knows the list to be very short (empty
 * or 1 entry) most of the time.
 */
#define __list_for_each(pos, head) \
	for (pos = (head)->next; pos != (head); pos = pos->next)

/**
 * list_for_each_prev	-	iterate over a list backwards
 * @pos:	the &struct list_head to use as a loop cursor.
 * @head:	the head for your list.
 */
#define list_for_each_prev(pos, head) \
	for (pos = (head)->prev; pos != (head); \
        	pos = pos->prev)

/**
 * list_for_each_safe - iterate over a list safe against removal of list entry
 * @pos:	the &struct list_head to use as a loop cursor.
 * @n:		another &struct list_head to use as temporary storage
 * @head:	the head for your list.
 */
#define list_for_each_safe(pos, n, head) \
	for (pos = (head)->next, n = pos->next; pos != (head); \
		pos = n, n = pos->next)

/**
 * list_for_each_entry	-	iterate over list of given type
 * @pos:	the type * to use as a loop cursor.
 * @head:	the head for your list.
 * @member:	the name of the list_struct within the struct.
 */
#define list_for_each_entry(pos, head, member)				\
	for (pos = list_entry((head)->next, typeof(*pos), member);	\
	     &pos->member != (head); 	\
	     pos = list_entry(pos->member.next, typeof(*pos), member))

/**
 * list_for_each_entry_reverse - iterate backwards over list of given type.
 * @pos:	the type * to use as a loop cursor.
 * @head:	the head for your list.
 * @member:	the name of the list_struct within the struct.
 */
#define list_for_each_entry_reverse(pos, head, member)			\
	for (pos = list_entry((head)->prev, typeof(*pos), member);	\
	     &pos->member != (head); 	\
	     pos = list_entry(pos->member.prev, typeof(*pos), member))

/**
 * list_prepare_entry - prepare a pos entry for use in list_for_each_entry_continue
 * @pos:	the type * to use as a start point
 * @head:	the head of the list
 * @member:	the name of the list_struct within the struct.
 *
 * Prepares a pos entry for use as a start point in list_for_each_entry_continue.
 */
#define list_prepare_entry(pos, head, member) \
	((pos) ? : list_entry(head, typeof(*pos), member))

/**
 * list_for_each_entry_continue - continue iteration over list of given type
 * @pos:	the type * to use as a loop cursor.
 * @head:	the head for your list.
 * @member:	the name of the list_struct within the struct.
 *
 * Continue to iterate over list of given type, continuing after
 * the current position.
 */
#define list_for_each_entry_continue(pos, head, member) 		\
	for (pos = list_entry(pos->member.next, typeof(*pos), member);	\
	     &pos->member != (head);	\
	     pos = list_entry(pos->member.next, typeof(*pos), member))

/**
 * list_for_each_entry_from - iterate over list of given type from the current point
 * @pos:	the type * to use as a loop cursor.
 * @head:	the head for your list.
 * @member:	the name of the list_struct within the struct.
 *
 * Iterate over list of given type, continuing from current position.
 */
#define list_for_each_entry_from(pos, head, member) 			\
	for (; &pos->member != (head);	\
	     pos = list_entry(pos->member.next, typeof(*pos), member))

/**
 * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry
 * @pos:	the type * to use as a loop cursor.
 * @n:		another type * to use as temporary storage
 * @head:	the head for your list.
 * @member:	the name of the list_struct within the struct.
 */
#define list_for_each_entry_safe(pos, n, head, member)			\
	for (pos = list_entry((head)->next, typeof(*pos), member),	\
		n = list_entry(pos->member.next, typeof(*pos), member);	\
	     &pos->member != (head); 					\
	     pos = n, n = list_entry(n->member.next, typeof(*n), member))

/**
 * list_for_each_entry_safe_continue
 * @pos:	the type * to use as a loop cursor.
 * @n:		another type * to use as temporary storage
 * @head:	the head for your list.
 * @member:	the name of the list_struct within the struct.
 *
 * Iterate over list of given type, continuing after current point,
 * safe against removal of list entry.
 */
#define list_for_each_entry_safe_continue(pos, n, head, member) 		\
	for (pos = list_entry(pos->member.next, typeof(*pos), member), 		\
		n = list_entry(pos->member.next, typeof(*pos), member);		\
	     &pos->member != (head);						\
	     pos = n, n = list_entry(n->member.next, typeof(*n), member))

/**
 * list_for_each_entry_safe_from
 * @pos:	the type * to use as a loop cursor.
 * @n:		another type * to use as temporary storage
 * @head:	the head for your list.
 * @member:	the name of the list_struct within the struct.
 *
 * Iterate over list of given type from current point, safe against
 * removal of list entry.
 */
#define list_for_each_entry_safe_from(pos, n, head, member) 			\
	for (n = list_entry(pos->member.next, typeof(*pos), member);		\
	     &pos->member != (head);						\
	     pos = n, n = list_entry(n->member.next, typeof(*n), member))

/**
 * list_for_each_entry_safe_reverse
 * @pos:	the type * to use as a loop cursor.
 * @n:		another type * to use as temporary storage
 * @head:	the head for your list.
 * @member:	the name of the list_struct within the struct.
 *
 * Iterate backwards over list of given type, safe against removal
 * of list entry.
 */
#define list_for_each_entry_safe_reverse(pos, n, head, member)		\
	for (pos = list_entry((head)->prev, typeof(*pos), member),	\
		n = list_entry(pos->member.prev, typeof(*pos), member);	\
	     &pos->member != (head); 					\
	     pos = n, n = list_entry(n->member.prev, typeof(*n), member))

#endif

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-01-03 16:22:02  更:2022-01-03 16:22:33 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 18:26:58-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码