首页
社区
课程
招聘
[原创]Windows进程/线程浅谈
发表于: 2006-6-27 17:31 22915

[原创]Windows进程/线程浅谈

EDD 活跃值
1
2006-6-27 17:31
22915

写在前面:
  这是俺个人学习的一些心得体会, 一直会持续更新, 独乐孰如与众乐? 提出来和大家分享, 有错误请排砖, 俺还可以更好的提高, 何乐不为? 其实写的时候最痛苦的就是发现很难孤立简单的谈一个问题. 举个例子, 文章第一部分中省略了进程地址空间的介绍, 主要就是考虑地址空间概念的复杂性(包含虚拟内存, 用户空间和系统空间概念etc.). 最害怕的就是有原则性的错误误导了初学者. 总结一句: 水平有限, 大家指教.

1. 进程的起源与演化
1.1 进程的起源
  进程, 多道程序设计技术, 分时技术是密不可分的整体. 多道程序设计思想是理论基础, 分时系统和进程为多道程序设计技术的实现提供了一个舞台.
  第一个问题: 为什么要有进程?
  在计算机发展史的早期时代, 硬件是非常昂贵, 非常稀缺的资源. 其中, 处理器的利用率是一个旗标. 硬件中断机制的出现, 又使处理器从繁重的外设操作中得到解放, 可以更多地作用于系统中运行的程序. 但单一程序在运行过程中却很可能停下来等待某个事件的发生, 例如等待外设完成某个操作, 等待用户的输入数据等等. 这种等待中的对处理机的巨大浪费难以令人忍受. 多道程序设计技术有效的解决了这个问题. 多道程序设计技术的基本思想是: 多个相互独立的程序在内存中同时存放, 相互穿插运行, 宏观并行, 微观串行. 那么如何保证多个程序相互穿插运行时能体现宏观上并行的假相呢? 这就需要分时技术的支持. 分时技术: 把处理机时间划分成很短的时间片, 时间片轮流地分配给各个程序. 而系统在实现多道运行的时候又怎么来定义各个宏观上并行的程序实体呢? 由此进程被引入了.
  进程定义多种多样, "进程是程序基于某一数据集合的一次运行活动" 是其中经典且被广泛接受的一种.

1.2 进程实体
  第二个问题: OK, 现在有了进程, 那进程又是一个什么样的实体?
  通过对多道程序设计技术, 分时技术, 进程慨念的一次总体衡量, 得到这样的结论:
  . 进程有一个主体--内存中的程序和数据集. 内存中的程序和数据集被称为程序的内存映像.
  . 进程是活动的/动态的实体, 具有生存周期.
  . 进程具有关联的处理机状态数据. 一般被称为进程的上下文.
  现在, 假设你是系统设计者, 你已经在你要设计的系统中引入了进程, 那么你肯定需要设计相应的数据结构来管理系统中的所有进程. 对应于一个进程的数据结构就是进程控制块, 它是系统内核中进程的表示.
  有了进程控制块, 就可以把进程的状态数据, 上下文等等都放入进程控制块中(引申一句: 进程的状态, 上下文等数据既可以看作进程的特征数据, 也可以被认为是进程管理的数据, 这是一个看问题角度不同而产生的差异), 那么现在进程就是由两部分组成: 内存映像和进程控制块. 进程控制块是系统中最核心, 也是最复杂的数据结构, 并且它在不同的系统中差别很大, 后面会比较详细的分析介绍Windows系统的进程控制块.

1.3 古典进程
  古典进程是俺自己发明的称呼, 主要针对轻量进程/线程出现之前的进程. 古典进程最大的特点是: 它是处理机资源分配的最小单位.

1.4 轻量进程 (Lightweight Processes)
  古典进程有几个很重大的缺陷, 其中之一是创建开销. 进程的创建开销分成两部分:
  a. 进程控制块的分配, 初始化.
  b. 内存映像的创建与初始化.(这里为了简化, 假设没有虚拟内存系统, 也暂时不考虑进程的地址空间问题)
  学习过UNIX操作系统原理的朋友可能记得, 在UNIX系统中, 调用fork创建一个新进程时, 系统会用父进程的映像初始化子进程的映像, 然后子进程可以调用exec API来重新填充它的映像. 此过程耗费大量的系统资源, 这种耗费表现在两方面: 一是通过拷贝父进程的映像创建子进程的映像, 二是子进程更改自己的映像.
  由此, 人们提出了轻量进程有时又称纤程的慨念. 早期的轻量进程和进程最大的不同在于创建轻量进程时, 系统并不用父进程的映像初始化子进程的映像, 而是要求父进程在创建时就指定所需的映像, 然后系统直接使用这个映像来创建子进程.
  古典进程的另一个重大缺陷是数据共享. 古典进程之间的数据共享只能通过进程间通讯来完成, 在大数据量, 多进程共享时效率偏低. 而且, 进程通讯需要进程切换, 也是一笔不小的开销. 这时可以看到, 轻量进程的发展目标就是能打破进程的边界, 共享它们的数据, 这不就是线程?

1.5 线程
  a. 线程的出现
  回答上面的问题: 轻量进程还不是线程. 线程的出现标志着系统中一个新的实体的出现(这是轻量进程所不具备的, 轻量进程仍然是进程), 同时线程实体的出现使进程实体的含义发生了重大改变, 是程序设计领域的一次革命性变化.
  俺的观点: 进程是相对独立程序间的多道并发设计技术, 线程是独立程序内部的多道并发设计技术. 线程技术的出现是实现独立程序内部多道并发需求的结果.
  b. 线程实体
  台湾译"Thread"为执行绪, 执行绪的意思就是一条执行线路, 和翻译为线程殊途同归. 从结构化程序设计的观点来看(尽管现代的操作系统中出现了越来越多基于对象的特征, 但系统本质上仍是结构化的, 所有基于对象的特征都是系统高层的一种封装表现), 程序是一个代码树, 线程的主体就是代码树上的一条路径, 是进程主体(进程的内存映像)的一个子集. 线程取代了进程成为处理机分配的最小单位, 所以它有上下文和状态.

2. Windows的进程和线程
2.1 WIndows进程和线程定义
  按照MS的定义, Windows中的进程简单地说就是一个内存中的可执行程序, 提供程序运行的各种资源. 进程拥有虚拟的地址空间, 可执行代码, 数据, 对象句柄集, 环境变量, 基础优先级, 以及最大最小工作集.
  Windows中的线程是系统处理机调度的基本单位. 线程可以执行进程中的任意代码, 包括正在被其他线程执行的代码. 进程中的所有线程共享进程的虚拟地址空间和系统资源. 每个线程拥有自己的例外处理过程, 一个调度优先级以及线程上下文数据结构. 线程上下文数据结构包含寄存器值, 核心堆栈, 用户堆栈和线程环境块.

2.2 进程的虚拟地址空间
  首先可以把"虚拟"这个词拿掉, 对于简化进程模型有很大帮助(当对进程/线程和虚拟内存部分都充分理解后, 那时结合起来再考量会有更深程度的掌握).
  地址空间就是系统给予进程的, 进程内任何可执行代码都可以感知的内存区域.(注意, 这里用了"感知"一词而不是访问, 其中的原因可以在后面讲解中找到)  
  问题: 地址空间有多大? 基地址和最大地址是什么?
  系统寻址能力决定了地址空间的大小. 主流的Windows系统是32位系统, 寻址能力是32位(形象的说就是指针是32位的), 所以地址空间的大小是4GB. 地址空间的基地址为0x00000000, 最大地址是0xFFFFFFFF. 4GB的地址空间被划分为2个大的内存区域, 用户区和系统区.
  问题: 为什么要有系统区?
  要回答这个问题就必需理解一个容易被忽视而又极端重要的概念: 操作系统是进程的一部分! 俺最早碰到这个观点是在学习BELL UNIX V系统培训资料时(书的作者是BELL实验室的主任, 名字忘记了, 后来一直也没有再找到这本书, 可惜). 当时感到特别的疑惑, 一方面的原因是当时对操作系统的了解是一个片面的, 狭义的理解, 另一方面就是被操作系统的复杂性弄昏了头脑, 反而忽视了一些基本的理念和原则. 于是, 俺给自己提了几个问题: 操作系统在运行时存放在内存的那里? 为什么要这么存放? 通过阅读操作系统的资料, 俺发现系统实际上被存放在进程的地址空间的一块区域中! 为什么要这样存放, 以下是俺个人的观点:
  1) 操作系统的一个基本目的就是把程序从类似于打开文件, 读取用户输入等等这样的烦琐的, 通用性的操作中解脱出来, 操作系统提供了很多服务例程来完成这些操作. 不考虑系统的管理功能的情况下, 操作系统就是服务例程的一个集合. 而服务例程从逻辑上, 从本质上看, 就是程序\进程的一部分.
  2) 管理例程从逻辑上来说也是程序\进程的一部分, 它实际上是系统代替程序\进程来完成这样的一个工作: 在多个程序\进程间, 依照一定的策略, 协调各种资源的分配, 使用.(大家需要明白处理机也是一种系统资源, 只不过它是特别重要的一种资源, 而且有一定的特殊性)
  3) 如果操作系统的管理例程在一个独立的进程中, 那么每次调用管理例程就需要一次进程切换, 这种开销是无法承受的. 而且这时管理例程和调用者的通讯是跨越进程边界的, 开销也很大.
  OK, 现在大家明白了在进程的地址空间中需要保留一部分区域给驻留的操作系统, 那么把地址空间划分为用户区和系统区也就是自然而然的做法啦. 现代操作系统上的系统分区一般都是被保护的, 这就是俺在定义地址空间时使用感知而不是访问的原因.
  实际上, WIndows的各个版本的分区情况也是各不相同的, 9x和NT有很大的差异, 而且不是简单的分为用户区和系统区, 还有类似于NULL指针区, 系统越界保护区等等. 这部分大家可以参考Richter的<<Windows核心编程>>, 书里讲的相当详细和严谨.
  地址空间是系统中相当重要的慨念, 当你对进程/线程管理, 虚拟内存机制有深入了解时, 你会发现地址空间描述了进程运行时内存的分布和构造, 揭示了系统运作中用户模块和系统模块的静态视图.

2.3 进程和线程的关系 -- 进程是容器?
  每个windows进程开始于它的被默认创建的第一个线程, 通常称其为主线程. 这种机制暗示了一个事实: 进程含有至少一个线程. 主线程和其它的线程没有任何区别, 每个线程都可以创建新的线程. 进程中所有线程都结束时进程会自动被结束, 而主动结束进程时, 如果还有线程没完成, 则系统自动结束这些线程.
  在windows中, 进程不再是处理机资源分配的最小单位, 那么它还是一个动态的实体吗? 是的, 从多进程并发的角度来看, 进程仍然是一个动态的实体, 但它的动态是它的线程的动态特征的抽象. 举个例子, 一个进程含有3个线程, 那么当3个线程都阻塞时, 进程表现为阻塞. 但只要有一个线程是就绪态, 哪怕其它2个线程是阻塞态, 进程仍然表现为就绪.
  从线程的角度看, 进程表现的更像一个容器, 它代表线程接受分配到的资源(除处理机资源), 为线程提供主体(执行代码+数据), 自己却没有运行的概念. 此时, 进程是静态的实体.

2.4 ETHREAD(KTHREAD), EPROCESS(KPROCESS)
  OK, 嘴皮子练了这么多(说的都干了), 总要搞点实际的代码玩玩, 老虎不发威, 不能当俺是病猫嘛.
  Windows中的进程控制块是EPROCESS结构, 线程控制块是ETHREAD结构. EPROCESS/ETHREAD的定义在inside windows2000中有比较详细的描述, 大家可以参考. 这里俺能给出的一些学习心得是:
  1) 在Dave Probert(这位老大是MS Windows核心模块组的牛人)的演讲稿中提及的一句: "Kernel implementation organized around the object manager". 翻译过来就是"核心是围绕对象管理来实现的". 俺觉得这是一个很重要的分析WIndows核心的视角, 而且似乎MS在积极的探索内核级的面向对象实现(当然MS要做到这一点很不容易, 兼容性是很大的问题).

  2) 俺一直没有弄的特别明白的是, 为什么要有KTHREAD, KPROCESS结构, 说实话, KTHREAD就是ETHREAD的头, KPROCESS就是EPROCESS的头. 俺感觉最可能的解释是: 存在核心线程这样的对象, 而核心线程的容器就是核心进程, 核心线程与用户线程是一一对应关系. 但从系统结构和核心调试结果来看, 似乎不存在独立的核心线程的概念, 只有线程经由系统陷入方式进入核心态. 下面的一段代码验证了俺的观点, 有意思的地方俺在代码中做了注释.

...
        pProc = IoGetCurrentProcess();

        // 很有意思, PEPROCESS->hreadListHead是个空链表
        pNext = pProc->ThreadListHead.Flink;
        DbgPrint("pProc->ThreadListHead.Flink = %p \n", pProc->ThreadListHead.Flink);
        DbgPrint("pProc->ThreadListHead.Blink = %p \n", pProc->ThreadListHead.Blink);

        pKProc = (PKPROCESS) pProc;
        pNext = pKProc->ThreadListHead.Flink;
        DbgPrint("kernel thread list begin \n");
        do
        {
                DbgPrint("pNext = %p \n", pNext);
                DbgPrint("thread: \n", pNext);
                pKThread = (PKTHREAD) (CONTAINING_RECORD(pNext, KTHREAD, ThreadListEntry));
                DbgPrint("pKThread = %p \n", pKThread);
                DbgPrint("pKThread->Teb = %p , %s \n", pKThread->Teb, pKThread->Teb == NULL || pKThread->Teb > 0x80000000 ? "System Thread" : "Non System Thread");
                //PTHREAD 指针需要从这里获得
                pThread = (PETHREAD) pKThread;
                DbgPrint("pThread->Cid = %d \n", pThread->Cid);
                nCount ++;
                pNext = pNext->Flink;       
        }while(pNext != &(pKProc->ThreadListHead));
        DbgPrint("kernel thread total count %d \n", nCount);
...

pProc->ThreadListHead.Flink = 00000000
pProc->ThreadListHead.Blink = 00000000
kernel thread list begin
pNext = FFB6B2E8
thread:
pKThread = FFB6B138
pKThread->Teb = 7FFDF000 , Non System Thread
pThread->Cid = 2000
pNext = 80CCB500
thread:
pKThread = 80CCB350
pKThread->Teb = 00000000 , System Thread
pThread->Cid = 960
kernel thread total count 2

  3) 补充一点, 还有一种线程被称为系统线程(system thread). 一个系统线程只运行在系统地址空间中, 所以它的TEB只能是NULL或大于0x80000000(其实写0x80000000是不严谨的, 因为这假设了系统是位于0x80000000之上, 实际上系统可能只占用最高的1GB). 所有的核心驱动程序运行在系统线程中.

2.4 进程链表
  1) Windows的进程链表是一个双向环链表
    这个环链表LIST_ENTRY结构把每个EPROCESS链接起来. 那么只要找到一个EPROCESS结构, 我们就可以遍历整个链表, 这就是枚举进程的原理. 下面是示例代码段和运行结果:

...
        pProc = IoGetCurrentProcess();
        pNext = pFirst = pProc->ActiveProcessLinks.Flink;
        DbgPrint("Process enumerate begin: \n", pNext);
        do
        {
                nCount ++;
                pProc = (PEPROCESS) (CONTAINING_RECORD(pNext, EPROCESS, ActiveProcessLinks));
                DbgPrint("--- process %d --- \n", nCount);
                DbgPrint("process id: %d \n", pProc->UniqueProcessId);
                DbgPrint("process image file: %s \n", pProc->ImageFileName);
                pNext = pNext->Flink;
                DbgPrint("\n");
               
        } while(pNext != pFirst);
        DbgPrint("proces total count %d \n", nCount);
        DbgPrint("Process enumerate end \n");
...

Process enumerate begin:
--- process 1 ---
process id: 2044
process image file: Dbgview.exe

--- process 2 ---
process id: 1092
process image file: drvContainer.ex

--- process 3 ---
process id: -2141872512
process image file:  

--- process 4 ---
process id: 4
process image file: System

--- process 5 ---
process id: 368
process image file: smss.exe

--- process 6 ---
process id: 568
process image file: csrss.exe

--- process 7 ---
process id: 596
process image file: winlogon.exe

--- process 8 ---
process id: 656
process image file: services.exe

--- process 9 ---
process id: 668
process image file: lsass.exe

--- process 10 ---
process id: 860
process image file: svchost.exe

--- process 11 ---
process id: 932
process image file: svchost.exe

--- process 12 ---
process id: 1020
process image file: svchost.exe

--- process 13 ---
process id: 1136
process image file: svchost.exe

--- process 14 ---
process id: 1192
process image file: svchost.exe

--- process 15 ---
process id: 1364
process image file: spoolsv.exe

--- process 16 ---
process id: 1668
process image file: VMwareService.e

--- process 17 ---
process id: 1908
process image file: alg.exe

--- process 18 ---
process id: 236
process image file: explorer.exe

--- process 19 ---
process id: 544
process image file: VMwareTray.exe

--- process 20 ---
process id: 572
process image file: VMwareUser.exe

--- process 21 ---
process id: 616
process image file: ctfmon.exe

--- process 22 ---
process id: 1180
process image file: wuauclt.exe

proces total count 22
Process enumerate end

  2) 从进程链表中移除进程块
  这个操作比较简单, 目前看起来也不会给系统造成大的影响, 但以后会不会出问题谁也说不定. 从进程链表中移除特定进程块后, 该进程仍能继续运行的原因是: 系统并不使用进程链表作为调度的查找起始点, 该进程的线程控制仍然存在于调度链表中. 下面是示例代码(代码中没有加入同步保护等内容, 是简化版的, 实际应用时不能如此简单, 否则可能发生不可预测的错误导致系统崩溃):
...
        pProc = IoGetCurrentProcess();
        pNext = pFirst = pProc->ActiveProcessLinks.Flink;
        do
        {
                pProc = (PEPROCESS) (CONTAINING_RECORD(pNext, EPROCESS, ActiveProcessLinks));
                if (pProc->UniqueProcessId == (HANDLE) TargetProcId)
                {
                        pProc->ActiveProcessLinks.Flink->Blink = pProc->ActiveProcessLinks.Blink;
                        pProc->ActiveProcessLinks.Blink->Flink = pProc->ActiveProcessLinks.Flink;
                        break;
                }
                pNext = pNext->Flink;
               
        } while(pNext != pFirst);
...
  有意思的是, 从进程链表中移除进程块的一个副作用是系统没法再枚举到这个进程.


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 7
支持
分享
最新回复 (23)
雪    币: 440
活跃值: (737)
能力值: ( LV9,RANK:690 )
在线值:
发帖
回帖
粉丝
2
支持原创
2006-6-27 17:43
0
雪    币: 235
活跃值: (41)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
3
支持一下,两下,。。。很多下
2006-6-27 21:08
0
雪    币: 207
活跃值: (40)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
4
还有纤程
CreateFiber
也很有意思
2006-6-27 22:02
0
雪    币: 603
活跃值: (617)
能力值: ( LV12,RANK:660 )
在线值:
发帖
回帖
粉丝
5
不错,继续啊,建议再扩充些内容,现在略显简单。
2006-6-28 09:55
0
雪    币: 224
活跃值: (40)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
6
谢谢大家的支持

to prince:
肯定会坚持写下去. 前面一部分是写的过于简单, 会适当修改, 当详细到什么程度实在是很难把握, 后面的windows部分会写的很详细.

Regards.
2006-6-28 10:21
0
雪    币: 224
活跃值: (40)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
7
  内容编辑到主贴中.
2006-6-28 13:44
0
雪    币: 1852
活跃值: (504)
能力值: (RANK:1010 )
在线值:
发帖
回帖
粉丝
8
写的不错
内容很短,最好添加到同一个主题贴,不要另开主题
2006-6-28 14:04
0
雪    币: 224
活跃值: (40)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
9
最初由 北极星2003 发布
写的不错
内容很短,最好添加到同一个主题贴,不要另开主题


嗯, 好建议, 写完这一部分就把所有的内容集中到一起.
2006-6-28 16:54
0
雪    币: 603
活跃值: (617)
能力值: ( LV12,RANK:660 )
在线值:
发帖
回帖
粉丝
10
4G的地址空间,高2G为系统空间,所有进程共享,低2G为用户空间,由分页机制分隔开各个进程。

建议版主,主题合并后加精吧,不然就打击了楼主的积极性了。
2006-6-29 09:49
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
好帖
2006-6-29 10:26
0
雪    币: 224
活跃值: (40)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
12
谢大家了, 争取写好, 对得起精品文章的称号.
2006-6-29 18:09
0
雪    币: 181
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
学习!不过 有点看不懂哈! 好帖子收藏了 看雪什么时候还准备出看雪纪念啊?
2006-6-29 20:21
0
雪    币: 1852
活跃值: (504)
能力值: (RANK:1010 )
在线值:
发帖
回帖
粉丝
14
不知道楼主的文章是否还要继续
文中大部分的内容是关于进程的,如果对文中的格式作些许修改即可以收录到“Windows系统程序设计”系列
如果有测试文件的源码,最好可以上传
2006-7-3 23:31
0
雪    币: 224
活跃值: (40)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
15
最初由 北极星2003 发布
不知道楼主的文章是否还要继续
文中大部分的内容是关于进程的,如果对文中的格式作些许修改即可以收录到“Windows系统程序设计”系列
如果有测试文件的源码,最好可以上传


  这篇文章俺还想继续更新一些学习心得. 斑竹的提议很好, 俺本来就觉得写的有些乱, 需要整理修改. 俺等会去“Windows系统程序设计”系列跟个贴, 认领进程管理这一块, 相关的源代码也会贴上来.
2006-7-4 09:58
0
雪    币: 603
活跃值: (617)
能力值: ( LV12,RANK:660 )
在线值:
发帖
回帖
粉丝
16
最初由 EDD 发布
这篇文章俺还想继续更新一些学习心得. 斑竹的提议很好, 俺本来就觉得写的有些乱, 需要整理修改. 俺等会去“Windows系统程序设计”系列跟个贴, 认领进程管理这一块, 相关的源代码也会贴上来.


支持!
2006-7-4 10:49
0
雪    币: 1852
活跃值: (504)
能力值: (RANK:1010 )
在线值:
发帖
回帖
粉丝
17
最初由 EDD 发布
这篇文章俺还想继续更新一些学习心得. 斑竹的提议很好, 俺本来就觉得写的有些乱, 需要整理修改. 俺等会去“Windows系统程序设计”系列跟个贴, 认领进程管理这一块, 相关的源代码也会贴上来.


多谢分享你的学习心得
2006-7-4 11:00
0
雪    币: 185
活跃值: (39)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
18
初来此地就被这篇文章深深的吸引住了!
2006-9-28 13:13
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
我想学习一下进程编程,可否告知一下,大虾用的是什么编程环境!我用大虾的程序片段时老是报错,缺东西,郁闷ing。
2006-10-9 17:09
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
学习!学习!xiexie,记着更新。
2006-10-9 18:25
0
雪    币: 234
活跃值: (104)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
好文!!!!!!!!1
2006-10-10 17:52
0
雪    币: 214
活跃值: (230)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
22
为什么线程是个馊主意:
http://www.softpanorama.org/People/Ousterhout/Threads/index.shtml
2006-10-13 22:22
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
23
我一口气看了三遍,终于有点懂了。真高兴,谢谢了。
2006-10-14 10:03
0
雪    币: 212
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
应该把代码上传上来,没代码不好学习啊
2006-10-30 10:51
0
游客
登录 | 注册 方可回帖
返回
//