玩转内核链表Llist_Head 教你管理不同类型节点的实现
玩转内核链表Llist_Head,教你管理不同类型节点的实现
作者:土豆居士 2022-09-15 11:51:28系统 Linux 虽然Linux内核是用C语言写的,但是list_head的引入,使得内核数据结构也可以拥有面向对象的特性,通过使用操作list_head 的通用接口很容易实现代码的重用,有点类似于C++的继承机制。在Linux内核中,提供了一个用来创建双向循环链表的结构 list_head。虽然linux内核是用C语言写的,但是list_head的引入,使得内核数据结构也可以拥有面向对象的特性,通过使用操作list_head 的通用接口很容易实现代码的重用,有点类似于C++的继承机制(希望有机会写篇文章研究一下C语言的面向对象机制)。
首先找到list_head结构体定义,kernel/inclue/linux/types.h 如下:
struct list_head {struct list_head *next, *prev;};#define LIST_HEAD_INIT(name) { &(name), &(name) }需要注意的一点是,头结点head是不使用的,这点需要注意。
使用list_head组织的链表的结构如下图所示:
然后就开始围绕这个结构开始构建链表,然后插入、删除节点 ,遍历整个链表等等,其实内核已经提供好了现成的接口,接下来就让我们进入 kernel/include/linux/list.h中:
一、创建链表
内核提供了下面的这些接口来初始化链表:
#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){WRITE_ONCE(list->next, list);list->prev = list;}如: 可以通过 LIST_HEAD(mylist) 进行初始化一个链表,mylist的prev 和 next 指针都是指向自己。
structlist_head mylist = {&mylist, &mylist} ;但是如果只是利用mylist这样的结构体实现链表就没有什么实际意义了,因为正常的链表都是为了遍历结构体中的其它有意义的字段而创建的,而我们mylist中只有 prev和next指针,却没有实际有意义的字段数据,所以毫无意义。
综上,我们可以创建一个宿主结构,然后在此结构中再嵌套mylist字段,宿主结构又有其它的字段(进程描述符 task_struct,页面管理的page结构,等就是采用这种方法创建链表的)。为简便理解,定义如下:
structmylist{int type;char name[MAX_NAME_LEN];struct list_head list;}创建链表,并初始化。
structlist_head myhead; INIT_LIST_HEAD(&myhead);这样我们的链表就初始化完毕,链表头的myhead就prev 和 next指针分别指向myhead自己了,如下图:
二、添加节点
内核已经提供了添加节点的接口了
1、list_add
如下所示。根据注释可知,是在链表头head后方插入一个新节点new。
/** * 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. */static inline void list_add(struct list_head *new, struct list_head *head){__list_add(new, head, head->next);}list_add再调用__list_add接口。
/* * Insert a new entry between two known consecutive entries. * * This is only for internal list manipulation where we know * the prev/next entries already! */static inline void __list_add(struct list_head *new,struct list_head *prev,struct list_head *next){if (!__list_add_valid(new, prev, next))return;next->prev = new;new->next = next;new->prev = prev;WRITE_ONCE(prev->next, new);}其实就是在myhead链表头后和链表头后第一个节点之间插入一个新节点。然后这个新的节点就变成了链表头后的第一个节点了。
接着上面步骤创建1个节点然后插入到myhead之后
struct mylist node1;node1.type = I2C_TYPE;strcpy(node1.name,"yikoulinux");list_add(&node1.list,&myhead);然后在创建第二个节点,同样把它插入到header_task之后。
struct mylist node2;node2.type = I2C_TYPE;strcpy(node2.name,"yikoupeng");list_add(&node2.list,&myhead);list_add
以此类推,每次插入一个新节点,都是紧靠着header节点,而之前插入的节点依次排序靠后,那最后一个节点则是第一次插入header后的那个节点。最终可得出:先来的节点靠后,而后来的节点靠前,“先进后出,后进先出”。所以此种结构类似于 stack“堆栈”, 而header_task就类似于内核stack中的栈顶指针esp,它都是紧靠着最后push到栈的元素。
2、list_add_tail 接口
上面所讲的list_add接口是从链表头header后添加的节点。同样,内核也提供了从链表尾处向前添加节点的接口list_add_tail.让我们来看一下它的具体实现。
/** * 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);}从注释可得出:
(1)在一个特定的链表头前面插入一个节点。
(2)这个方法很适用于队列的实现(why?)。
进一步把__list_add()展开如下:
/* * Insert a new entry between two known consecutive entries. * * This is only for internal list manipulation where we know * the prev/next entries already! */static inline void __list_add(struct list_head *new,struct list_head *prev,struct list_head *next){if (!__list_add_valid(new, prev, next))return;next->prev = new;new->next = next;new->prev = prev;WRITE_ONCE(prev->next, new);}所以,很清楚明了, list_add_tail就相当于在链表头前方依次插入新的节点(也可理解为在链表尾部开始插入节点,此时,header节点既是为节点,保持不变)
利用上面分析list_add接口的方法可画出数据结构图形如下。
(1)创建一个链表头(实际上应该是表尾)代码参考第一节。
(2)插入第一个节点 node1.list , 调用。
struct mylist node1; node1.type = I2C_TYPE; strcpy(node1.name,"yikoulinux"); list_add_tail(&node1.list,&myhead);(3) 插入第二个节点node2.list,调用。
list_add_tail
依此类推,每次插入的新节点都是紧挨着 header_task表尾,而插入的第一个节点my_first_task排在了第一位,my_second_task排在了第二位,可得出:先插入的节点排在前面,后插入的节点排在后面,“先进先出,后进后出”,这不正是队列的特点吗(First in First out)!
三、删除节点
内核同样在list.h文件中提供了删除节点的接口 list_del(), 让我们看一下它的实现流程。
static inline void list_del(struct list_head *entry){__list_del_entry(entry);entry->next = LIST_POISON1;entry->prev = LIST_POISON2;}/* * 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;WRITE_ONCE(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. */static inline void __list_del_entry(struct list_head *entry){if (!__list_del_entry_valid(entry))return;__list_del(entry->prev, entry->next);}利用list_del(struct list_head *entry) 接口就可以删除链表中的任意节点了,但需注意,前提条件是这个节点是已知的,既在链表中真实存在,切prev,next指针都不为NULL。
四、链表遍历
内核是同过下面这个宏定义来完成对list_head链表进行遍历的,如下 :
/** * 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_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.h 中也提供了list_replace(节点替换) list_move(节点移位),翻转,查找等接口,这里就不在一一分析了。
五、宿主结构
1、找出宿主结构 list_entry(ptr, type, member)
上面的所有操作都是基于list_head这个链表进行的,涉及的结构体也都是:
struct list_head {struct list_head *next, *prev;};其实,正如文章一开始所说,我们真正更关心的是包含list_head这个结构体字段的宿主结构体,因为只有定位到了宿主结构体的起始地址,我们才能对对宿主结构体中的其它有意义的字段进行操作。
struct mylist{ int type;char name[MAX_NAME_LEN];struct list_head list;};那我们如何根据list这个字段的地址而找到宿主结构node1的位置呢?list.h中定义如下:
/** * 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_head within the struct. */#define list_entry(ptr, type, member) \container_of(ptr, type, member)list.h中提供了list_entry宏来实现对应地址的转换,但最终还是调用了container_of宏,所以container_of宏的伟大之处不言而喻。
2、container_of
做linux驱动开发的同学是不是想到了LDD3这本书中经常使用的一个非常经典的宏定义!
container_of(ptr, type, member)在LDD3这本书中的第三章字符设备驱动,以及第十四章驱动设备模型中多次提到,我觉得这个宏应该是内核最经典的宏之一。那接下来让我们揭开她的面纱:
此宏在内核代码 kernel/include/linux/kernel.h中定义(此处kernel版本为3.10;新版本4.13之后此宏定义改变,但实现思想保持一致)。
而offsetof定义在kernel/include/linux/stddef.h,如下:
举个例子,来简单分析一下container_of内部实现机制。
例如:
struct test{int a;short b;charc;};structtest*p = (struct test *)malloc(sizeof(structtest));test_function(&(p->b));inttest_function(short *addr_b){//获取struct test结构体空间的首地址 structtest *addr; addr = container_of(addr_b,struct test,b);}展开container_of宏,探究内部的实现:
typeof (( (struct test *)0 )->b ) ;(1)typeof (( (struct test *)0 )->b ) *__mptr =addr_b ;(2)(struct test *)( (char *)__mptr-offsetof(struct test,b)) (3)(1) 获取成员变量b的类型 ,这里获取的就是short 类型。这是GNU_C的扩展语法。
(2) 用获取的变量类型,定义了一个指针变量 __mptr ,并且将成员变量 b的首地址赋值给它。
(3) 这里的offsetof(struct test,b)是用来计算成员b在这个struct test 结构体的偏移。__mptr是成员b的首地址, 现在 减去成员b在结构体里面的偏移值,算出来的是不是这个结构体的首地址呀 。
3、宿主结构的遍历
我们可以根据结构体中成员变量的地址找到宿主结构的地址,并且我们可以对成员变量所建立的链表进行遍历,那我们是不是也可以通过某种方法对宿主结构进行遍历呢?
答案肯定是可以的,内核在list.h中提供了下面的宏:
/** * 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_head within the struct. */#define list_for_each_entry(pos, head, member)\for (pos = list_first_entry(head, typeof(*pos), member);\ &pos->member != (head);\ pos = list_next_entry(pos, member))其中,list_first_entry 和 list_next_entry宏都定义在list.h中,分别代表:获取第一个真正的宿主结构的地址;获取下一个宿主结构的地址。它们的实现都是利用list_entry宏。
/** * list_first_entry - get the first element from a list * @ptr:the list head to take the element from. * @type:the type of the struct this is embedded in. * @member:the name of the list_head within the struct. * * Note, that list is expected to be not empty. */#define list_first_entry(ptr, type, member) \list_entry((ptr)->next, type, member)/** * list_next_entry - get the next element in list * @pos:the type * to cursor * @member:the name of the list_head within the struct. */#define list_next_entry(pos, member) \list_entry((pos)->member.next, typeof(*(pos)), member)最终实现了宿主结构的遍历。
#define list_for_each_entry(pos, head, member)\for (pos = list_first_entry(head, typeof(*pos), member);\ &pos->member != (head);\ pos = list_next_entry(pos, member))首先pos定位到第一个宿主结构地址,然后循环获取下一个宿主结构地址,如果查到宿主结构中的member成员变量(宿主结构中struct list_head定义的字段)地址为head,则退出,从而实现了宿主结构的遍历。如果要循环对宿主结构中的其它成员变量进行操作,这个遍历操作就显得特别有意义了。
我们用上面的 nod结构举个例子:
struct my_list *pos_ptr = NULL ; list_for_each_entry (pos_ptr, &myhead, list ) {printk ("val =%d\n" , pos_ptr->val); }实例1 一个简单的链表的实现
为方便起见,本例把内核的list.h文件单独拷贝出来,这样就可以独立于内核来编译测试。
功能描述:
本例比较简单,仅仅实现了单链表节点的创建、删除、遍历。
#include "list.h" #include运行结果:
实例2 如何在一个链表上管理不同类型的节点
功能描述:
本实例主要实现在同一个链表上管理两个不同类型的节点,实现增删改查的操作。
结构体定义
一个链表要想区分节点的不同类型,那么节点中必须要有信息能够区分该节点类型,为了方便节点扩展,我们参考Linux内核,定义一个统一类型的结构体:
struct device{int type;char name[MAX_NAME_LEN];struct list_head list;};其中成员type表示该节点的类型:
#defineI2C_TYPE 1 #define SPI_TYPE 2有了该结构体,我们要定义其他类型的结构体只需要包含该结构体即可,这个思想有点像面向对象语言的基类,后续派生出新的属性叫子类,说到这,一口君又忍不住想挖个坑,写一篇如何用C语言实现面向对象思想的继承、多态、interface。
下面我们定义2种类型的结构体:
i2c这种类型设备的专用结构体:
struct i2c_node{int data;unsigned int reg;struct device dev;};spi这种类型设备的专用结构体:
struct spi_node{unsigned int reg;struct device dev;};我特意让两个结构体大小类型不一致。
结构类型
链表头结点定义
structlist_head device_list;根据之前我们讲解的思想,这个链表链接起来后,应该是以下这种结构:
节点的插入
我们定义的节点要插入链表仍然是要依赖list_add(),既然我们定义了struct device这个结构体,那么我们完全可以参考linux内核,针对不同的节点封装函数,要注册到这个链表只需要调用该函数即可。
实现如下:
设备i2c的注册函数如下:
void i2c_register_device(struct device*dev){dev.type = I2C_TYPE;strcpy(dev.name,"yikoulinux");list_add(&dev->list,&device_list);}设备spi的注册函数如下:
void spi_register_device(struct device*dev){dev.type = SPI_TYPE;strcpy(dev.name,"yikoupeng");list_add(&dev->list,&device_list);}我们可以看到注册函数功能是填充了struct device 的type和name成员,然后再调用list_add()注册到链表中。这个思想很重要,因为Linux内核中许许多多的设备节点也是这样添加到其他的链表中的。要想让自己的C语言编程能力得到质的提升,一定要多读内核代码,即使看不懂也要坚持看,古人有云:代码读百遍其义自见。
节点的删除
同理,节点的删除,我们也统一封装成函数,同样只传递参数device即可:
void i2c_unregister_device(struct device *device){//struct i2c_node *i2c_device=container_of(dev, struct i2c_node, dev);list_del(&device->list);}void spi_unregister_device(struct device *device){//struct spi_node *spi_device=container_of(dev, struct spi_node, dev);list_del(&device->list);}在函数中,可以用container_of提取出了设备节点的首地址,实际使用中可以根据设备的不同释放不同的资源。
宿主结构的遍历
节点的遍历,在这里我们通过设备链表device_list开始遍历,假设该节点名是node,通过list_for_each()可以得到node->dev->list的地址,然后利用container_of 可以得到node->dev、node的地址。
void display_list(struct list_head *list_head){int i=0;struct list_head *p;struct device *entry;printf("-------list---------\n");list_for_each(p,list_head){printf("node[%d]\n",i++);entry=list_entry(p,struct device,list);switch(entry->type){case I2C_TYPE: display_i2c_device(entry);break;case SPI_TYPE:display_spi_device(entry);break;default:printf("unknown device type!\n");break;}display_device(entry);}printf("-------end----------\n");}由以上代码可知,利用内核链表的统一接口,找个每一个节点的list成员,然后再利用container_of 得到我们定义的标准结构体struct device,进而解析出节点的类型,调用对应节点显示函数,这个地方其实还可以优化,就是我们可以在struct device中添加一个函数指针,在xxx_unregister_device()函数中可以将该函数指针直接注册进来,那么此处代码会更精简高效一些。如果在做项目的过程中,写出这种面向对象思想的代码,那么你的地址是肯定不一样的。读者有兴趣可以自己尝试一下。
void display_i2c_device(struct device *device){struct i2c_node *i2c_device=container_of(device, struct i2c_node, dev);printf("\ti2c_device->data: %d\n",i2c_device->data);printf("\ti2c_device->reg: %#x\n",i2c_device->reg);}void display_spi_device(struct device *device){struct spi_node *spi_device=container_of(device, struct spi_node, dev);printf("\tspi_device->reg: %#x\n",spi_device->reg);}上述代码提取出来宿主节点的信息。
实例代码
#include "list.h" #include代码主要功能:
117-118:定义两个不同类型的节点dev1,dev2。120 :初始化设备链表。121-122、124:初始化节点数据。123/125:向链表device_list注册这两个节点。126:显示该链表。127:删除节点dev1。128 :显示该链表。程序运行截图
读者可以试试如何管理更多类型的节点。
实例3 实现节点在两个链表上自由移动
功能描述:
初始化两个链表,实现两个链表上节点的插入和移动。每个节点维护大量的临时内存数据。
节点创建
节点结构体创建如下:
struct mylist{int number;char type;char *pmem;//内存存放地址,需要mallocstruct list_head list;};需要注意成员pmem,因为要维护大量的内存,我们最好不要直定义个很大的数组,因为定义的变量位于栈中,而一般的系统给栈的空间是有限的,如果定义的变量占用空间太大,会导致栈溢出,一口君曾经就遇到过这个bug。
链表定义和初始化
链表定义如下:
structlist_head active_head; struct list_head free_head;初始化:
INIT_LIST_HEAD(&free_head);INIT_LIST_HEAD(&active_head);这两个链表如下:
关于节点,因为该实例是从实际项目中剥离出来,节点启示是起到一个缓冲去的作用,数量不是无限的,所以在此我们默认最多10个节点。
我们不再动态创建节点,而是先全局创建指针数组,存放这10个节点的地址,然后将这10个节点插入到对应的队列中。
数组定义:
structmylist*list_array[BUFFER_NUM];这个数组只用于存放指针,所以定义之后实际情况如下:
初始化这个数组对应的节点:
static ssize_t buffer_ring_init(){int i=0;for(i=0;i5:为下标为i的节点分配实际大小为sizeof(structmylist)的内存。
6:初始化该节点的链表。
7:为pmem成员从堆中分配一块内存。
初始化完毕,链表实际情况如下:
节点插入
static ssize_t insert_free_list_all(){int i=0;for(i=0;i8:用头插法将所有节点插入到free_head链表中。
所有节点全部插入free链表后,结构图如下:
遍历链表
虽然可以通过数组遍历链表,但是实际在操作过程中,在链表中各个节点的位置是错乱的。所以最好从借助list节点来查找各个节点。
show_list(&free_head);show_list(&active_head)代码实现如下:
void show_list(struct list_head *list_head){int i=0;struct mylist*entry,*tmp;//判断节点是否为空if(list_empty(list_head)==true){return;}list_for_each_entry_safe(entry,tmp,list_head,list){printf("[%d]=%d\t",i++,entry->number);if(i%4==0){printf("\n");}}}节点移动
将节点从active_head链表移动到free_head链表,有点像生产者消费者模型中的消费者,吃掉资源后,就要把这个节点放置到空闲链表,让生产者能够继续生产数据,所以这两个函数我起名eat、spit,意为吃掉和吐,希望你们不要觉得很怪异。
int eat_node(){struct mylist*entry=NULL;if(list_empty(&active_head)==true){printf("list active_head is empty!-----------\n");}entry=list_first_entry(&active_head,struct mylist,list);printf("\t eat node=%d\n",entry->number);list_move_tail(&entry->list,&free_head);}节点移动的思路是:
1. 利用list_empty判断该链表是否为空。
2. 利用list_first_entry从active_head链表中查找到一个节点,并用指针entry指向该节点。
3. 利用list_move_tail将该节点移入到free_head链表,注意此处不能用list_add,因为这个节点我要从原链表把他删除掉,然后插入到新链表。
将节点从free_head链表移动到active_head链表。
spit_node(){struct mylist*entry=NULL;if(list_empty(&free_head)==true){printf("list free_head is empty!-----------\n");}entry=list_first_entry(&free_head,struct mylist,list);printf("\t spit node=%d\n",entry->number);list_move_tail(&entry->list,&active_head);}大部分功能讲解完了,下面我们贴下完整代码。
代码实例
#include运行结果如下:
list_head短小精悍,读者可以借鉴此文实现其他功能。
责任编辑:姜华 来源:一口Linux list_headLinux推荐系统
微软Win11原版22H2下载_Win11GHOST 免 激活密钥 22H2正式版64位免费下载
语言:中文版系统大小:5.13GB系统类型:Win11微软Win11原版22H2下载_Win11GHOST 免 激活密钥 22H2正式版64位免费下载系统在家用办公上跑分表现都是非常优秀,完美的兼容各种硬件和软件,运行环境安全可靠稳定。Win11 64位 Office办公版(免费)优化 1、保留 Edge浏览器。 2、隐藏“操作中心”托盘图标。 3、保留常用组件(微软商店,计算器,图片查看器等)。 5、关闭天气资讯。
Win11 21H2 官方正式版下载_Win11 21H2最新系统免激活下载
语言:中文版系统大小:4.75GB系统类型:Win11Ghost Win11 21H2是微软在系统方面技术积累雄厚深耕多年,Ghost Win11 21H2系统在家用办公上跑分表现都是非常优秀,完美的兼容各种硬件和软件,运行环境安全可靠稳定。Ghost Win11 21H2是微软最新发布的KB5019961补丁升级而来的最新版的21H2系统,以Windows 11 21H2 22000 1219 专业版为基础进行优化,保持原汁原味,系统流畅稳定,保留常用组件
windows11中文版镜像 微软win11正式版简体中文GHOST ISO镜像64位系统下载
语言:中文版系统大小:5.31GB系统类型:Win11windows11中文版镜像 微软win11正式版简体中文GHOST ISO镜像64位系统下载,微软win11发布快大半年了,其中做了很多次补丁和修复一些BUG,比之前的版本有一些功能上的调整,目前已经升级到最新版本的镜像系统,并且优化了自动激活,永久使用。windows11中文版镜像国内镜像下载地址微软windows11正式版镜像 介绍:1、对函数算法进行了一定程度的简化和优化
微软windows11正式版GHOST ISO镜像 win11下载 国内最新版渠道下载
语言:中文版系统大小:5.31GB系统类型:Win11微软windows11正式版GHOST ISO镜像 win11下载 国内最新版渠道下载,微软2022年正式推出了win11系统,很多人迫不及待的要体验,本站提供了最新版的微软Windows11正式版系统下载,微软windows11正式版镜像 是一款功能超级强大的装机系统,是微软方面全新推出的装机系统,这款系统可以通过pe直接的完成安装,对此系统感兴趣,想要使用的用户们就快来下载
微软windows11系统下载 微软原版 Ghost win11 X64 正式版ISO镜像文件
语言:中文版系统大小:0MB系统类型:Win11微软Ghost win11 正式版镜像文件是一款由微软方面推出的优秀全新装机系统,这款系统的新功能非常多,用户们能够在这里体验到最富有人性化的设计等,且全新的柔软界面,看起来非常的舒服~微软Ghost win11 正式版镜像文件介绍:1、与各种硬件设备兼容。 更好地完成用户安装并有效地使用。2、稳定使用蓝屏,系统不再兼容,更能享受无缝的系统服务。3、为
雨林木风Windows11专业版 Ghost Win11官方正式版 (22H2) 系统下载
语言:中文版系统大小:4.75GB系统类型:雨林木风Windows11专业版 Ghost Win11官方正式版 (22H2) 系统下载在系统方面技术积累雄厚深耕多年,打造了国内重装系统行业的雨林木风品牌,其系统口碑得到许多人认可,积累了广大的用户群体,雨林木风是一款稳定流畅的系统,一直以来都以用户为中心,是由雨林木风团队推出的Windows11国内镜像版,基于国内用户的习惯,做了系统性能的优化,采用了新的系统
雨林木风win7旗舰版系统下载 win7 32位旗舰版 GHOST 免激活镜像ISO
语言:中文版系统大小:5.91GB系统类型:Win7雨林木风win7旗舰版系统下载 win7 32位旗舰版 GHOST 免激活镜像ISO在系统方面技术积累雄厚深耕多年,加固了系统安全策略,雨林木风win7旗舰版系统在家用办公上跑分表现都是非常优秀,完美的兼容各种硬件和软件,运行环境安全可靠稳定。win7 32位旗舰装机版 v2019 05能够帮助用户们进行系统的一键安装、快速装机等,系统中的内容全面,能够为广大用户
番茄花园Ghost Win7 x64 SP1稳定装机版2022年7月(64位) 高速下载
语言:中文版系统大小:3.91GB系统类型:Win7欢迎使用 番茄花园 Ghost Win7 x64 SP1 2022.07 极速装机版 专业装机版具有更安全、更稳定、更人性化等特点。集成最常用的装机软件,集成最全面的硬件驱动,精心挑选的系统维护工具,加上独有人性化的设计。是电脑城、个人、公司快速装机之首选!拥有此系统
相关文章
- 高手教你怎样修好U盘的MBR
- win7工作组怎么查看
- Win8无法启动IE提示“服务器正在运行中”怎么办?
- QQ头像图片设置为透明的图文详细说明_腾讯QQ
- win7 wifi 静态ip
- 找到被盗QQ号里的所有好友的办法_腾讯QQ
- Win8.1打开或关闭屏幕键盘声音的步骤
- 安装win7系统之后键盘鼠标不能用 完美解决方法
- win7怎样安装鼠标指针
- 而且也进不去main函数 代码、运行结果如下
- 电脑声音怎么设置 如何设置电脑音频管理器
- Windows10系统下桌面文件拖动不了怎样办?
- Win11怎么设置屏幕休眠时间?Win11设置屏幕休眠时间的步骤
- Win8交换机怎样设置?设置交换机的办法
- win7怎么在桌面显示日历?win7系统将日历显示在桌面上的方法
- 微信收钱图文详细教程_微信
- Win7笔记本电池充不满怎么办?
- Windows10系统无法激活报错“0xffffffff”的处理办法
热门系统
- 1华硕笔记本&台式机专用系统 GhostWin7 32位正式旗舰版2018年8月(32位)ISO镜像下载
- 2深度技术 Windows 10 x86 企业版 电脑城装机版2018年10月(32位) ISO镜像免费下载
- 3电脑公司 装机专用系统Windows10 x86喜迎国庆 企业版2020年10月(32位) ISO镜像快速下载
- 4雨林木风 Ghost Win7 SP1 装机版 2020年4月(32位) 提供下载
- 5深度技术 Windows 10 x86 企业版 六一节 电脑城装机版 版本1903 2022年6月(32位) ISO镜像免费下载
- 6深度技术 Windows 10 x64 企业版 电脑城装机版2021年1月(64位) 高速下载
- 7新萝卜家园电脑城专用系统 Windows10 x64 企业版2019年10月(64位) ISO镜像免费下载
- 8新萝卜家园 GhostWin7 SP1 最新电脑城极速装机版2018年8月(32位)ISO镜像下载
- 9电脑公司Ghost Win8.1 x32 精选纯净版2022年5月(免激活) ISO镜像高速下载
- 10新萝卜家园Ghost Win8.1 X32 最新纯净版2018年05(自动激活) ISO镜像免费下载
热门文章
常用系统
- 1电脑公司 GHOST WIN10 X86 装机专业版 V2017.09 (32位) 下载
- 2风林火山 GHOST Win7 SP1 贺岁旗舰版 V2012.01(32位) 下载
- 3雨林木风 GHOST WIN7 SP1 X86 装机稳定版 V2023.05(32位) 下载
- 4番茄花园Ghost Win10 64位 大神装机版 v2023.09免费最新下载
- 52022版Win10系统下载_2022版Win10最新版镜像下载
- 6番茄花园 GHOST WIN7 SP1 X64 优化正式版 V2023.07 下载
- 7番茄花园Ghost Win10 绿色安装版x64 v2023.03最新下载
- 8番茄花园 Windows 10 官方企业版 2021年3月(64位) ISO高速下载
- 9老机专用Win10系统下载_老机专用超流畅Win10 64位最新版下载
- 10深度技术 Ghost Win7 x64 Sp1 电脑城纯净版2019年4月(64位) ISO镜像快速下载