不会开机的男孩

Win32汇编学习(1)

| Comments

终于把学校里面让人无语的论文搞定了。周末终于有时间干些自己想干的事了。想起了这2周做的关于编译原理的实验,代码优化这部分的确是个难题。哎,我实在是太笨了,其实答案就在自己电脑里。将c程序反汇编后,终于第一次感受到了debug 和release的区别。兴奋之余让我又产生了忧虑,自己汇编的能力太弱了,面对稍微复杂点的算法再加上编译优化后的汇编代码,真是欲哭无泪。痛下决心,准备好好学学汇编了。为了给自己一个动力,准备学习Win32汇编(和学校那个8086再见了)。

  第一天,不准备上难度了。从最简单的”HelloWorld”开始。

; HelloWorld.asm

comment * ----------------------------------------------
                 The First Assemble Application
                ---------------------------------------------- *

.386
.model flat, stdcall
option casemap:none

include \masm32\include\windows.inc

include \masm32\include\user32.inc
include \masm32\include\kernel32.inc

includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

    .data
szCaption   db 'MessageBox', 0
szText      db 'Hello, World!', 0

    .code
start:
    invoke MessageBox,\     ; 调用函数名
        NULL,\                      ; 父窗口句柄
        offset szText,\           ; 文字
        offset szCaption,\      ; 标题
        MB_OK                    ; 按钮类型

    invoke ExitProcess, NULL
end start

一个简单的窗口就创建好了。一眼看上去发现和以前的8086还是有很多不同的。

首先8086和80386在寻址方式不同。

8086 通过 段地址*0x10 +偏移地址确定的。只能寻址1M,而80386 32根地址线寻址,空间达到了4G而且80386 通用寄存器大小为32位,所以不需要分段就能访问到地址。

那么.data,.code不是段的意思么? 不是。因为808386有分页机制,每个页可以自由制定属性,已经和8086代码和数据分段处理完全不同,实际上是把不同类型的数据或代码归类,再放到不同属性的内存页。

其次,8086的不安全,不方便的调用中断的方式改为了调用系统API的方式。我这里是采用的MASM,所以有invoke伪指令,其实这个指令也没别的,就是为我们编程省去了函数参数入栈的那些push。

最后,发现win32汇编和使用c/c++似乎没有复杂很多。

多线程程序设计笔记二

| Comments

当我们正式开始之前,我想再多说一点,上一篇最后的那个程序可能会给像我一样的菜鸟一个误解,这里解释下。

程序启动后就执行的那个线程称为主线程(也就是那个程序中的执行main函数的线程),而其他线程则成为子线程。主线程和其他线程最大的区别是当主线程返回或是调用一些函数强制退出后,使得程序中的其他子线程强制结束。 在一篇中native code和Manager code 在遇到同样的问题时,.net给我们做了一个好的榜样,其实不管是否是UI子线程,.net 控制台在终止进程结束之前,都会保证所有子线程已经退出。虽然我们无法知道到底它采用的是什么机制(我的理解是在调用结束进程之前调用了WaitForMultipleObjects或是其他wait函数等待所有子线程返回),但是这体现了一个很重要的原则,对产生的子线程负责,无论什么时候,都不应该直接结束程序而不等待子线程结束。因为子线程没有机会做清理工作。这个是一个非常可怕的问题,想这样的一个情况,子线程比如正在申请一个堆空间,而正好锁住了那个区域,然后被强制结束了。那么就很可能产生了内存泄露(产生不了也会增大系统负担)。所以再次提醒自己主线程在退出的时候,必须保证所有的子线程已经退出。

从上一篇《多线程程序设计笔记一》中,我们知道了多线程程序设计的最基础的知识。下面总结下线程之间的通讯和同步问题。不过这个问题实在是太大了,对我来说。这里先只涉及最简单的在同一进程下的用户方式下的同步问题。

以下内容将包括:

互锁函数 临界区 互锁函数

互锁函数运行在用户模式。它能保证当一个线程访问一个变量时,其它线程无法访问此变量,以确保变量值的唯一性。

下面是一个简单的例子。

DWORD WINAPI ThreadFunc1(PVOID n)
{
  while(InterlockedExchange(&g_bResourceInUse,TRUE) == TRUE)
    SwitchToThread();
  printf("thread1 used\n");
  InterlockedExchange(&g_bResourceInUse,FALSE);
  return 0;
}
DWORD WINAPI ThreadFunc2(PVOID n)
{
  while(InterlockedExchange(&g_bResourceInUse,TRUE) == TRUE)
    SwitchToThread();
  printf("thread2 used\n");
  InterlockedExchange(&g_bResourceInUse,FALSE);    
  return 0;
}

这个例子通过不断地判断bResourceInUse中的信息来确定线程是否能够使用资源。但是使用这个方法必须小心。大量的循环运算会浪费宝贵的CPU时间。而且如果是在单CPU下,线程不可能真正的异步执行,在thread1判断while的时候,thread2并不能做什么(不能修改该值)。所以我们应该避免在单个CPU计算机上使用循环锁。

这里面还有一个需要知道的是必须使用关键字volatile声明g_bResourceInUse。我们需要把循环锁变量和循环锁保护的数据维护在不同的高速缓存行中。通过高速缓存行CPU可以不必访问内存总线而获得数据,但是在多处理器环境中,高速缓存行使得内存更新更加困难。如下:

CPU1读取一个字节,将该字节和相邻字节读入CPU1的高速缓存行。 CPU2读取同一个字节。从而和第一步相同的内容读入了CPU2的高速缓存行。 CPU1修改该字节,因为已经在高速缓存行中,所以修改后的内容写入CPU1的高速缓存行,这个信息还没有写入内存。 CPU2再次入去同一个字节。因为已经放入了CPU2的高速缓存行,所以CPU2不会访问内存。那么问题出现了。这个字节并不是该字节的新值。 这个问题的确很严重,不过硬件工作者已经给我们解决了这个问题,当一个CPU修改高速缓存行字节时,其他CPU会被告知这个情况,他们的高速缓存行将无效。所以第四步中,CPU1必须将高速缓存行转入内存,而CPU2必须再次访问内存。

原因想说清楚这些我现在还不行,这又涉及到了多核编程(哎,愧对老师啊,《多核程序设计》那课是白学了)。不过这里还是必须解释下volatile。

被定义为volatile的变量,每次从内存中读取,而不能把他放在cache或寄存器中重复使用。 告知编译器不要对这个变量做优化。 告知编译器,变量可以被应用程序本身以外的某个东西进行修改,这些东西包括操作系统,硬件或同时执行的线程等。

当必须以原子操作方式修改32为,64位值时,我们可以使用互锁函数。他们很有效率。但是实际工作,我们需要面对更复杂的数据结构。而且他们的效率是不进入内核态而节省下的。如果等待资源时间过长,就变成对CPU极大的浪费了,我们需要一种机制,使线程在等待访问共享资源时不浪费CPU时间。

临界区(Critical Section)又叫做关键代码段

临界区的描述

win32提供的一种轻量级的同步机制,它存在于进程的内存空间中。一次只有一个线程获准进入临界区执行代码段,(其实就是让若干行代码能够以原子操作方式来使用资源)。 它并不总是执行向内核模式的控制转换,要是获得一个未被占用的临界区时,只需要在用户态内的很少运算就能完成,只有在尝试获得已占用临界区时,它才会跳至内核模式。 只能在属于同一个进程的线程间同步。 补充一点,比如当线程A试图进入线程B拥有的临界区时,线程A将被置于等待状态。线程B离开临界区,线程A将处于可调度状态。让线程A立即等待,并不一定立即切换到内核方式。MS为了提高关键代码段的运行性能,将循环锁加入了这些代码段。当调用 EnterCriticalSection 它使用循环锁进行循环,只有当每次尝试获取都失败时才转入内核方式,从而线程A进入等待状态。

这里可能就又糊涂了。之前说的循环锁不是效率很低么?的确,但是如果和转入内核方式比所消耗的资源少的话,就是可行的。比如我们仅仅是想操作一个指针。当然,如果是在单CPU下,循环锁是没有意义的,会直接转入内核方式。

临界区的使用方法

    通过 InitializeCriticalSection 或 InitializeCriticalSectionAndSpinCount 函数初始化一个 CRITICAL_SECTION 结构,使用 SetCriticalSectionSpinCount 函数设置临界区的Spin计数器(可选)。然后使用 EnterCriticalSection 或 TryEnterCriticalSection 获取临界区的所有权;完成需要同步的操作后,使用 LeaveCriticalSection 函数释放临界区。最后使用 DeleteCriticalSection 函数析构临界区结构(只是删除RTL_CRITICAL_SECTION_ DEBUG)。

讲了这么多理论,实践一下。

下面是对上一篇List做的多线程改进。

typedef struct _Node
{
  struct _Node *next;
  int data;
}Node;
typedef struct _List
{
  Node *head;
  CRITICAL_SECTION sec;
}List;
List *CreateList()
{
  List *pList = (List *)malloc(sizeof(pList));
  pList->head = NULL;
  InitializeCriticalSection(&pList->sec);
  return pList;
}
void DeleteList(List *pList)
{
  DeleteCriticalSection(&pList->sec);
  free(pList);
  pList = NULL;
}
void AddHead(List *pList,Node *node)
{
  EnterCriticalSection(&pList->sec);
  node->next = pList->head;
  pList->head = node;
  LeaveCriticalSection(&pList->sec);
}

当然事实上没有这么简单。比如当交换两个链表内容的函数

void SwapLists(List *list1,List *List2)
{
  List *temp_List;
  EnterCriticalSection(list1->sec);
  EnterCriticalSection(list2->sec);
  tmp_List->list = list1->head;
  list1->head = list2->head;
  list2->head = temp->list;
  LeaveCriticalSection(list1->sec);
  LeaveCriticalSection(list2->sec);
}

当threadA: SwapLists(list1,list2);threadB:SwapLists(list2,list1)。两个线程会落入“我等你,你等我”的轮回,这种情况称为死锁。

任何时候当一段代码需要1个以上的资源时,都可能发生死锁。而我们防止死锁通常的做法是保证“all or nothing”,也就是要不全部拥有,要不什么也没有。

其实上面的代码还隐藏了一个问题,SwapList函数在使用的时候无形的需要确保一个资源使用的顺序。也就是说这个函数的运程依赖于代码的执行顺序,这种设计本身就是很脆弱的。

临界区需要注意的

每个共享资源使用一个CRITICAL_SECTION。只有被临界区Enter和Leave“围起来”的资源才能获得保护,临界区维护的只是一段代码(代码中通常有一些资源)。 当同时访问多个资源的时候,使用临界区非常容易造成死锁, EnterCriticalSection 的顺序是需要认真考虑的但是并不一定十分可靠, LeaveCriticalSection 顺序则没有关系。 不要长时间运行临界区,也就是不要长时间锁住一个资源。但是时间到底多长很难确定在windows OS中,所以不要在一个CRITICAL_SECTION中调用Sleep或Wait….API函数,SendMessage。当你以一个同步机制保护一份资源时,必须牢记“这项资源被使用频率如何?线程必须多块释放资源,才能确保整个程序运作平顺”。 无法获知进入临界区的线程状态。由于临界区不是核心对象,如果一个线程进入临界区后,没有Leave,系统没有办法清除临界区。而且如果一个线程在Enter时被等待,那么等待的最长时间也是不能设定的。(也减少了一个处理错误的方式) 想要了解更多的关于临界区的,参考http://msdn.microsoft.com/zh-cn/magazine/cc164040(en-us).aspx

中文http://www.microsoft.com/china/MSDN/library/enterprisedevelopment/softwaredev/ousCriticalSections.mspx?mfr=true

多线程程序设计笔记一

| Comments

多线程编程,学习了一个星期,总结一下。以下内容全部基于windows操作系统。 由于实力有限,对操作系统没有深入了解(或是根本不了解吧),一下内容主要来自《windows核心编程》、《win32多线程程序设计》、一些网上高人的见解和自己的理解而自己的认识难免有些偏差,希望大家挑挑毛病。谢谢。

进程和线程的区别。

进程通常被定义为一个正在运行的程序实例,它由两个部分组成

1.操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。 2.地址空间,它饱含所有可执行模块或DLL模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。

线程是CPU调度的基本单位。由两个部分组成的

  1. 一个是线程的内核对象,操作系统用它来对线程实施管理。内核对象是系统用来存放线程统计信息的地方。
  2. 线程堆栈,它用于维护线程在执行代码时需要的所有函数参数和局部变量。

单个进程可能包含多个线程,而线程都“同时”执行进程地址空间中的代码,为此每个线程都有它自己的一组CPU寄存器(称为线程的上下文)和它自己的堆栈。进程是不活泼的,从不执行任何东西,它只是线程的容器。操作系统为每个线程安排一定的CPU时间片,仿佛所有线程都是同时运行一样。在同一个进程下的多个线程,能够执行相同的代码,对相同的数据进行操作,共享内核对象句柄。

Context Switch

在一个抢占式多任务操作系统中,操作系统确保每个线程都有机会执行,它依赖硬件的协助以及其他的工作。当硬件计时器认为某个线程已经执行够久了,就会发出一个中断,与之CPU保存线程的当前状态,把所有寄存器内容复制到堆栈中,再把它从堆栈中复制到CONTEXT结构中。操作系统通过恢复CONTEXT结构中的寄存器值来切换不同的线程。(当然得先切换到该线程隶属的进程内存)。这个过程为Context Switch 。当然如果有非常多的CPU,也就不需要进行Context Switch。

使用多线程的原因。

  1. 和进程相比,它是一种非常”节俭”的操作方式。启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
  2. 线程间方便的通信机制。同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用。
  3. 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作和UI线程工作分开。
  4. 线程彼此分享了大部分核心对象的拥有权。
  5. 在多CPU下,多个线程可以真正的同时运行,提高了CPU的利用率。

多线程所带来的问题

  1. 多线程在单CPU上实际上是一个假象,CPU的时间总是有限的,如果用的不好,反而增加了CPU的负担,降低了系统性能。
  2. 多线程发生共享数据空间,在这个抢占式环境下线程运行次序无法预期,容易产生竞争,导致程序效率降低,出错,甚至死锁。
  3. 过多的线程导致过多的Context Switch,也会降低运行效率。

讲了这么多,可见多线程程序设计给程序员带来了更高的要求。再查MSDN的时候,有的API会附上线程安全而有的却没有。下面我们开始进入多线程编程世界。

在进入多线程世界之前,先想一个问题。

设想一下,假如有一个简单的增添链表的操作

AddHead(struct List *list,struct Node *node)
{
   node -> next = List->head;
   List->head = node;
}

这个程序若是在多线程下进行的话,会出问题。 比如当thread1 正在执行 AddHead,而Context Switch发生在node->next = List->head 和 List->head = node语句之间,而thread2也要执行AddHead,而且正确执行之后,并把一个节点添加到List中,那么当Context Switch发生,而thread1执行List->head = node后,那么thread2添加的节点就不在List中了,造成了内存泄露,而且的确,你可以想到很多出错的可能性。你可能说这个问题出现的概率很低,但是多想一下,若是这个程序是在一个搜索引擎中,每天有多少人去运行这个程序?而且CPU是以每秒千万级(我的YY,反正是很快)运行,那么程序的问题就不可避免。而之所以产生了问题,就是因为thread1和thread2产生了竞争(race condition)。

当然可能你一下子就有了思路,产生这个问题的关键是在于在发生race 的地方设置变量,来“保护”AddHead的操作正确。

AddHead(struct List *list,struct Node *node)
{
   while(flag !=0)
   flag = 1;
   node -> next = List->head;
   List->head = node;
   flag = 0;
}

这个代码似乎可以正确运行,但是很遗憾,还是不行。好吧让我们先来明确一个知识。Atomic Operations(原子操作)。 一下是AddHead的汇编代码

AddHead(struct List *list,struct Node *node)
{
  xor eax,eax 
  s: 
  ;while(flag !=0) 
  cmp DWORD PTR _flag,eax 7   
  jne short s 
  ;flag = 1; 
  mov eax,DWORD PTR _list$[esp-4]
  mov ecx,DWORD PTR _node$[esp-4]
  mov DWORD PTR _flag,1
  ;node -> next = List->head;
  mov edx,DWORD PTR [eax]
  mov DWORD PTR [ecx],edxx
  ;List->head = node;
  mov DWORD PTR [eax],ecx
  ;flag = 0;
  mov DWORD PTR _flag,0
  ret 0
   ;小弟还没学习完汇编,有一些操作码还是不知道,只能是理解意思,不能保证
  ;肯定能执行,例子来自《Win32多线程程序设计》
;}

# Atomic Operations(原子操作)

简单的几行代码在执行时会转换成这么多代码,不能指望C,C++这种高级语言的语句可以一次执行完毕而不发生Context Switch。

而且还存在编译器优化(比如一些循环语句等等)真实在机器上执行的代码可能和你所希望的是不同的。

原子操作(Atomic Operation )指一个操作如果能够不受中断的完成。

所以上例的检查标记和设立标记的动作必须是一个Atomic Operation,如果中断了,就不能避免竞争。

好吧让我们真正的开始多线程编程旅行吧,我们和学习其他任何知识一样,来一个HelloWorld。

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

DWORD WINAPI ThreadFunc(LPVOID);

int main()
{
    HANDLE hThrd;
    DWORD threadId;
    hThrd = CreateThread(NULL,
,
            ThreadFunc,
,//(LPVOID)i,
,
            &threadId );
    if (hThrd)
    {
      printf("Thread launched\n");
      CloseHandle(hThrd);
    }
    //Sleep(2000);
    return 0;
}

DWORD WINAPI ThreadFunc(LPVOID n)
{
  printf("HelloWorld\n");
  return 0;
}

运行后发现悲剧了,Thread launched有了,但是没有我们的熟悉HelloWorld(对大部分人来说是没机会看到HelloWorld的),那么我们又遇到什么问题了?

加上Sleep后,HelloWorld出现了,好吧,我们又发现了,创建一个线程,并写一个调用函数,和我们在main函数中写一个子程序不是一回事。

一个函数调用操作,程序的控制权转移到被调函数中,执行完毕后在返回原调用处。

产生一个线程,情况也十分类似,但是有些曲折,线程函数中我们是通过CreateThread,并传给ThreadFunc的地址。CreateThread开启一个新的线程,该线程调用ThreadFunc而原来的线程继续前进。好吧。ThreadFunc相对main来说异步执行了。那么ThreadFunc不需要在main结束之前返回,所以,main函数返回了,ThreadFunc还没来得及返回,所以没有显示HelloWorld,而Sleep之后,那么ThreadFunc返回了,那么也就显示HelloWorld。

当main函数返回后,操作系统中止整个进程,收回大部分或全部资源,而你的thread就还没工作完就被“干”掉了。

当然你也可能会产生另一个疑问,main函数返回了,进程结束了,thread就直接悲剧了,能不能让main函数返回,进程不结束,thread不悲剧呢?

 static void Main(string[] args)
 {
   Thread th = new Thread(ShowWindow);
   th.Start();
   Console.WriteLine("窗体已创建,敲任意键退出...");
   Console.ReadKey();
   Console.WriteLine("Main thread End");
 }
  static void ShowWindow()
 {
   MessageBox.Show("test");
 }

这是一段C#代码,主要来自ref我稍微改动了一点点,这段代码可以保证即使主线程退出了,只要窗体没有关闭,操作系统会认为“进程”仍在执行,因此,控制台窗口会保持显示,直到窗体关闭,整个进程才结束。在这种情况下,本示例程序中有两个UI线程,一个是控制台窗口,另一个创建应用程序窗体的那个线程。

如下是我模仿上边代码,写的c++的例子

 int main()
 {
   HANDLE hThrd;
   DWORD threadId;
   HANDLE handle;
   hThrd = CreateThread(NULL,
           0,
           ThreadFunc,
           0,//(LPVOID)i,
            0,
           &threadId );
   if(hThrd)
   {
     printf("Thread launched\n");
     CloseHandle(hThrd);
   }
   Sleep(5000);
   printf("Main Thread End\n");
   return 0;
 }
 DWORD WINAPI ThreadFunc(LPVOID n)
 {
   MessageBox(NULL,"test","test",MB_OK);
   printf("thread1 End\n");
   return 0;
 }

令人遗憾的是,我写的这个,在main函数返回后,整个进程也随之结束了。。。。。。

好吧。我承认我错了,到底哪里出了问题。我是搞不定了。这个花了我大概2天的时间,曾经我一度以为UI线程和Message queue之间有某种必然的联系,但是上例完全证明了我之前的想法是可笑的或是我整个思考出发点就是错误的。在thread1中不仅调用了GDI函数,而且能够响应消息(说明已经有了Message queue和Message loop)。

看来我得继续学习才能搞定这个了。

缩略图设计初探二

| Comments

之前的问题还是很大的,参考了.net framework Dictionary的思路,保留MPQ的hash算法和判断冲突的思路。重新整理了一下。

第一部分:改进部分

1、处理冲突的方法由原来线性再散列,改为分离链表法。

2、修正了一些因为处理冲突而变化的部分。

3、增加了扩容的部分。

4、增加了CRC校验部分。

第二部分:疑问

1、如何保证数据的安全性?Delete操作只是将数据从hashTable中delete,但是文件中依然有图片存在。

要么将每个图片加密储存,但是手机上资源消耗太大。那么最有可能就是delete之后将文件数据格式中关键数据上写一些随机数,从而正常解码失败。

2、如果要统一管理图片,需要建立文件索引。

3、过多的seek会影响效率,我这里还是保存了文件的偏移量,在读取的时候也必然会seek,因为我觉得使用seek似乎没有在效率产生很大的损失(可能我测试的数据不具有普遍性吧),但是很多资料上都对寻道很讨厌。这里我又参考了一下空间问题,故而保存了文件偏移量。

第三部分:部分代码

插入

BOOL MPFile::InsertData( LPCVOID lpBuffer, TCHAR *lpszString, const UINT fileSize, const FILETIME LastModiTime, 
        LONG &lindex, DWORD dwFlags )
{
    ASSERT( lpBuffer != NULL );
    ASSERT( lpszString != NULL );

    if( m_pMPBlockTable == NULL ) Initialize(0);
    const DWORD HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;   
    DWORD hashCode = hash.HashString(lpszString, HASH_OFFSET);   
    DWORD nHashA = hash.HashString(lpszString, HASH_A);   
    DWORD nHashB = hash.HashString(lpszString, HASH_B);   

    DWORD targetBucket = hashCode % (m_pMPFileHeader->nHashTableLength); 
    for ( LONG i = m_pMPBlucketTable[targetBucket].iBlockIndex; i >= 0; i = m_pMPBlockTable[i].MPEntry.iNext ) 
    {
        if( m_pMPBlockTable[i].MPEntry.dwHashCode == hashCode
        &&m_pMPBlockTable[i].MPEntry.dwHashValueA == nHashA 
        && m_pMPBlockTable[i].MPEntry.dwHashValueB == nHashB )
        {
        if( !(dwFlags & INSERT_REPLACE_EXISTING) ) {return FALSE;}
            return SetDataToFile(lpBuffer,lpszString,i);
        }
    }
    // add new 
    LONG index;
    BOOL isNew=TRUE;
    if (m_pMPFileHeader->nFreeCount > 0) 
    { 
        index = m_pMPFileHeader->iFreeList;
        m_pMPFileHeader->iFreeList = m_pMPBlockTable[index].MPEntry.iNext;
        m_pMPFileHeader->nFreeCount--;
        isNew = FALSE;
    } 
    else 
    {
        if (m_pMPFileHeader->nChildFileCount == m_pMPFileHeader->nHashTableLength) 
        {
            ReSize();
            targetBucket = hashCode % (m_pMPFileHeader->nHashTableLength);
        }
        index = m_pMPFileHeader->nChildFileCount;
        m_pMPFileHeader->nChildFileCount++;
    } 

    m_pMPBlockTable[index].MPEntry.dwHashCode= hashCode; 
    m_pMPBlockTable[index].MPEntry.iNext = m_pMPBlucketTable[targetBucket].iBlockIndex; 
    m_pMPBlockTable[index].MPEntry.dwHashValueA = nHashA;
    m_pMPBlockTable[index].MPEntry.dwHashValueB = nHashB;
    m_pMPBlockTable[index].dwFlag = dwFlags;
    m_pMPBlockTable[index].nSize = fileSize + sizeof(MINIPICDATAITEMHEADER)+sizeof(DWORD);//size add fileName and crc32
    m_pMPBlucketTable[targetBucket].iBlockIndex = index;

    lindex = index;

    //TODO:修改数据
     if(isNew)
    {
        m_pMPBlockTable[index].FileStartAt = m_endOfFileData;
        if(SetDataToFile(lpBuffer,lpszString,index) )
        {
            m_endOfFileData+=m_pMPBlockTable[index].nSize;//TODO:防止溢出
            return TRUE;
        }
        return FALSE;
    }
    else
    {
        return SetDataToFile( lpBuffer,lpszString, index );
    }
}

读取数据

LONG MPFile::FindEntry( TCHAR *lpszString )
{
    ASSERT(lpszString!=NULL);
     if (m_pMPBlucketTable != NULL ) 
     {
        const DWORD HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;   
        DWORD    hashCode = hash.HashString(lpszString, HASH_OFFSET);   
        DWORD    nHashA = hash.HashString(lpszString, HASH_A);   
        DWORD    nHashB = hash.HashString(lpszString, HASH_B);   
        DWORD    nHashStart = hashCode % (m_pMPFileHeader->nHashTableLength);   

        for (LONG i = m_pMPBlucketTable[nHashStart].iBlockIndex; i >= 0; i = m_pMPBlockTable[i].MPEntry.iNext) 
        {
            if (m_pMPBlockTable[i].MPEntry.dwHashCode == hashCode 
                &&m_pMPBlockTable[i].MPEntry.dwHashValueA == nHashA 
                && m_pMPBlockTable[i].MPEntry.dwHashValueB == nHashB )
            return i;
        }
     }
     return -1;
}

MP_v2.rar源码

缩略图设计初探

| Comments

第一部分简介:

最近有幸参加了一个网上的开源项目,需要我设计一个缩略图存储的方法。整个思路主要是模仿XP的实现方式,而对于Win7(或是Vista)里面将所有缩略图统一管理的模式没有采用。主要想法还是一切从简,这也就是初探的来由了。

整个缩略图的核心算法来自《Inside MoPaQ》http://shadowflare.samods.org/inside_mopaq/

核心是将文件名通过Hash散列到表中,从而达到快速查找的目的。

MPQ文件,对于大部分大学时间沉浸的游戏——War3是非常重要的,每次War3版本的更替都对这个文件做了修改。的确,对于一个计算机系的非常爱玩的我,现在才分析MPQ实在是惭愧。

对于我这个文件名,现在姑且命名为MP(MiniPic)。

这是我第一次用c++来写程序,整个程序很简单,花了差不多1个半月(我这个效率实在不行),和第一篇一样,希望各位大哥能帮忙看看。

第二部分实现:

  1. 需求

1.1 这里实现读取,修改固定大小的文件,即每个缩略图的存储空间大小一致。

1.2 根据文件名添加,查找,删除功能。

1.3 文件存储数目固定,不能动态增长。

1.4 储存文件名,使用ID形式访问。

2.存储方案

2.1 表结构设计

整个文件包括FileHeader,HashTable,BlockTable,DataHeader,Data构成。通过计算文件名的hash值,将文件名散列到HashTable中,然后根据HashTable中查找BlockTable(文件块表),在BlockTable中查找到实际文件data的位置。读入DataHeader和Data。

FileHeader HashTableItem HashTableItem …… HashTableItem

BlockTableItem BlockTableItem DataHeader Data DataHeader

Data …

2.2 内存中存储。

内存中存储FileHeader、HashTable、BlockTable、DebrisBlock,dataBuffer。

DebrisBlock是队列结构。存储因删除操作而产生的碎片。

FileHeader,HashTable,BlockTable是数组。dataBuffer数据缓存

3.实现方案

实际还需要增加功能,以下是核心部分。对于每个功能的算法也只写核心部分。

3.1 增加

3.1.1 将文件名转换为数字。这里使用的是MPQ文件算法。

DWORD HashManager::HashString( TCHAR *lpszFileName,DWORD dwHashType )
{
    BYTE *key = (BYTE *)lpszFileName;   
    DWORD seed1 = 0x7FED7FED, seed2 = 0xEEEEEEEE;   
    int ch;   

    while(*key != 0)   
    {
        ch = toupper(*key++);
        //原来是char,这里处理TCHAR中的0,这里也有小问题,如果要更多的地方使用,还需要做大头小头机数据转换
         if(!*key)
        {
            ++key;
        }
        seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2);   
        seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3;    
    }   
    return seed1; 
}

3.1.2 插入HashTable。

在处理冲突的时候,是采用最简单的线性探查法,在判断位置相同的时候,采用增加2个Hash值的方法。虽然不能保证肯定不会出问题。但是3个相同的概率很低,大概是10的22.3次方分之一,这个是在参考《Inside MoPaQ》,中的数据,是否可靠。我无法确定。

BOOL HashManager::InsertHashTable( TCHAR *lpszString, FILETIME FileTime, LONG &HashPos, DWORD dwFlags,
                                   BOOL &isNew//设置,是否是新文件
)
{
    ASSERT( m_pHashIndexTable!= NULL );
    isNew = TRUE;
    const DWORD HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;   
    DWORD nHash = HashString(lpszString, HASH_OFFSET);   
    DWORD nHashA = HashString(lpszString, HASH_A);   
    DWORD nHashB = HashString(lpszString, HASH_B);   
    DWORD nHashStart = nHash %m_hashTableLength;   
    DWORD nHashPos = nHashStart;   
    DWORD x=m_pHashIndexTable[nHashPos].dwFlag & FILE_EXISTS;
    DWORD y = m_pHashIndexTable[nHashPos].dwFlag & FILE_DELETED;
    while( x!=0 )
    {   
        if( y==FILE_DELETED )
        {
            isNew = FALSE;
            break;
        }
        //如果有同名文件
         if (m_pHashIndexTable[nHashPos].dwHashValueA == nHashA                && m_pHashIndexTable[nHashPos].dwHashValueB == nHashB)
        {
            if( dwFlags & INSERT_CHECK_FILETIME )
            {
                if(m_pHashIndexTable[nHashPos].dwFileLastModiTime.dwHighDateTime == FileTime.dwHighDateTime
                   && m_pHashIndexTable[nHashPos].dwFileLastModiTime.dwLowDateTime == FileTime.dwLowDateTime)
                {
                  return FALSE;//说明文件真实存在,插入失败
                  }
                else
                {
                    isNew = FALSE;
                    break;
                }
            }
            if( dwFlags & INSERT_REPLACE_EXISTING )
            {
                isNew = FALSE;
                break;
            }
            else
            {
                return FALSE;
            }
        }  
        nHashPos = (nHashPos + 1) % m_hashTableLength;   
        if(nHashPos == nHashStart)    
        {   
            return FALSE;    
        }   
        x=m_pHashIndexTable[nHashPos].dwFlag & FILE_EXISTS;
        y = m_pHashIndexTable[nHashPos].dwFlag & FILE_DELETED;
    }   
    m_pHashIndexTable[nHashPos].dwFlag = FILE_EXISTS;  
    m_pHashIndexTable[nHashPos].dwHashValueA = nHashA;   
    m_pHashIndexTable[nHashPos].dwHashValueB = nHashB;   
    m_pHashIndexTable[nHashPos].iBlockIndex=nHashPos;
    if( dwFlags & INSERT_CHECK_FILETIME )
    {
        m_pHashIndexTable[nHashPos].dwFileLastModiTime.dwHighDateTime = FileTime.dwHighDateTime;
        m_pHashIndexTable[nHashPos].dwFileLastModiTime.dwLowDateTime = FileTime.dwLowDateTime;
    }
    HashPos = nHashPos;
    return TRUE;  
}

3.1.3 插入BlockTable

如果DebrisBlock(空闲表)为空,则将BlockTable中的文件偏移量指向文件最后。

如果DebrisBlock不为空,则将BlockTable中的文件偏移量指向BlockTable头结点,DebrisBlock头结点出列。

3.2 查找

3.2.1 查找HashTable

LONG HashManager::GetHashTablePos( TCHAR *lpszString )
{
    const DWORD HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;   
    DWORD nHash = HashString(lpszString, HASH_OFFSET);   
    DWORD nHashA = HashString(lpszString, HASH_A);   
    DWORD nHashB = HashString(lpszString, HASH_B);   
    DWORD nHashStart = nHash % m_hashTableLength;   
    DWORD nHashPos = nHashStart; 

    DWORD x=m_pHashIndexTable[nHashPos].dwFlag & FILE_EXISTS;
    while( x != 0 )
    {    
        if(m_pHashIndexTable[nHashPos].dwHashValueA == nHashA               && m_pHashIndexTable[nHashPos].dwHashValueB == nHashB)
        {
            return nHashPos;  
        }      
        else
        {
            nHashPos = (nHashPos + 1) % m_hashTableLength; 
        }  
        if(nHashPos == nHashStart)    
             break;    
        x=m_pHashIndexTable[nHashPos].dwFlag & FILE_EXISTS;
    }
    return -1; //没有找到
}

3.3 删除

3.3.1 查找HashTable :GetHashTablePos;

3.3.2 查找到则DebrisBlock入列文件偏移量,

3.3.3 在HashTable中Flag标记位FILE_DELETED

3.3.4 备注:这里不是真正删除。真正删除在MPFileClose()中的ManagerFileDebris进行。

BOOL    MiniPic::DeleteData(TCHAR *lpszString)
{
    ASSERT(m_isInit);
    ASSERT( m_pHashManager != NULL );
    ASSERT( lpszString != NULL );
    LONG hashTablePos;
    hashTablePos = m_pHashManager->GetHashTablePos(lpszString);
    if( hashTablePos == -1 )
    {
        return FALSE;
    }
    MINIPICDEBRIS debris;
    debris.dwBlockTableIndex = hashTablePos;
    debris.dwDebrisStartAt = m_pBlockTableManager->GetBlockTable()[hashTablePos].FileStartAt;
    m_pHashManager->DeleteHashTable(hashTablePos);
    m_debrisBlock.push_back(debris);
    DebugPrintString(L"Free data the pos is %d\n",hashTablePos);
#ifdef TEST
    if(count1>=testNum)
    {
        count1=0;
    }
    testIns[count1++]=hashTablePos;
#endif
    m_fileHeader.childFileCount--;
    return TRUE;
}

3.4 回收资源

因为在删除的时候只是在hashTable中做了一个Deleted的标记,并没有真正删除文件。若是长时间使用,则会浪费很多空间。此时需要整理。

收集空闲区域。排序,将文件尾部数据移动至最早空闲处,删除空闲处,更新hashTable。

BOOL MiniPic::ManagerFileDebris( LPVOID lpBuffer, const UINT count )
{
    ASSERT( lpBuffer != NULL );
    ASSERT( m_MPFile != NULL );
    if( m_debrisBlock.empty() )
    {
        return TRUE;
    }
    MINIPICDATAITEMHEADER dataItemHeader;
    m_debrisBlock.sort();

    /*
首先将比较Debris block末尾,若是处在文件的最后,则将Debris block末尾数据出列。并向前移动fileEndPointer     保证Debris block要小于文件末尾。
确认Debris block是否为空。
将末尾文件数据移动到Debris block的首部位置,fileEndPointer--,并将Debris block首部数据出列。
    在判断条件1,2*/
    //查找Debris。
    while( !m_debrisBlock.empty() )
   {
        while( m_debrisBlock.back().dwDebrisStartAt + count                   + sizeof(MINIPICDATAITEMHEADER) == m_fileDataEndPointer )//处理文件末尾的空闲区域
         {
            m_fileDataEndPointer -= count + sizeof(MINIPICDATAITEMHEADER);
            m_pHashManager->ClearHashTable( m_debrisBlock.back().dwBlockTableIndex );//New add
            m_debrisBlock.pop_back();
            if( m_debrisBlock.empty() )
            {
                break;
            }
        }
        if( !m_debrisBlock.empty() )
        {
            if( 0xFFFFFFFF == m_MPFile->Seek( m_fileDataEndPointer - count - sizeof(MINIPICDATAITEMHEADER), 
                FILE_BEGIN ))//指向最后文件块开始处
              {
                return FALSE;
            }
            m_MPFile->Read( &dataItemHeader, sizeof(MINIPICDATAITEMHEADER) );
            m_MPFile->Read( lpBuffer, count );
            if( 0xFFFFFFFF == m_MPFile->Seek( m_debrisBlock.front().dwDebrisStartAt, FILE_BEGIN ) )
            {
                return FALSE;
            }
            m_MPFile->Write( &dataItemHeader, sizeof(MINIPICDATAITEMHEADER) );
            m_MPFile->Write( lpBuffer, count );
            m_fileDataEndPointer -= count+sizeof(MINIPICDATAITEMHEADER);
            m_pBlockTableManager->SetBlockTableFileStartAt( dataItemHeader.dwBlockTableIndex,
            m_debrisBlock.front().dwDebrisStartAt );
            //将原来指向空此间的HashTable标记位空。
              m_pHashManager->ClearHashTable( m_debrisBlock.front().dwBlockTableIndex );
            m_debrisBlock.pop_front();
            DebugPrintString(L"find freedata set in it ,and the pos is %d\n",dataItemHeader.dwBlockTableIndex);
        }
    }
    m_fileHeader.bfSize = m_fileDataEndPointer;
    return TRUE;
}

第三部分 使用

BOOL      MPFileOpen( LPCTSTR lpFileName );

void          MPFileClose();                            

BOOL      MPFileAddFile( LPCVOID lpBuffer, TCHAR *lpszString, const DWORD fileSize,

                 const FILETIME LastModiTime, LONG &lIndex, DWORD dwFlags

);

BOOL       MPFileReadFile( LPVOID lpBuffer, TCHAR *lpszString );

BOOL       MPFileDeleteFile(TCHAR *lpszString);

void           MPFileFlushData();

BOOL       MPFileReadFileByIndex( LPVOID lpBuffer, const DWORD index );

BOOL       MPFileGetFileName( TCHAR *FileNameBuffer, const  size_t count,

                                                        const DWORD index

);

BOOL        MPFileReName( LPCTSTR lpNewFileName, LPCTSTR lpOldFileName);

第四部分 未解决问题

没有实现动态增长,处理文件的最大值有限。 没有对文件正确性做足够保证。没有加入CRC or MD5校验。 没有设定返回的错误码只有TRUE,FALSE,不利于识别错误问题。 算法的有些地方效率不高。还需改进。 数据的大小没有做限制,插入过多数据会溢出。

第五部分 疑问

在处理非常多的数据时,是否该将文件压缩? 许多管理文件系统都加入了最近访问文件列表,这些在有大量文件读入时效率高,是否可以设计类似结构? 如何处理异常,特别是在写入时产生的异常,导致文件写入错误。如何能够更好的解决类似问题?在发生异常的时候,如何能够做到高效的完全释放资源? 把HashTable,BlockTable放置到文件末尾,方便文件以后动态扩展。但是具体的如何扩容hashTable,可能不好办。 在文件头部分增加一个Flag,标示文件的状态。(在读入的时候标记位0,最后如果没有异常则标记位1,那么如果在这个文件出现了问题,那么就可以通过检测这个Flag来判断文件异常),若是设计的更好,可以通过这个检测那部分有问题,而对问题修正。 我之前谈到的异常情况,导致有的文件没有即时更新。我的想法是来自IE浏览器,他会记录上次出错的位置,那么这时候就可以启动恢复程序。要是MP文件的Flag是正常,那么就说明问题不在MP文件处理,可能是别的地方异常导致结束,那么就可以重新查找,根据文件创建的时间来判断是否文件变化过,那么这样就比重新生成要快多了。我这里应该提供用来恢复的接口,这个由外部程序调用它。 第六部分 写给自己

如果仔细看的话,这个并不是很复杂的东西,只是实现了最基本最简单的内容,但是自己做的依然很不完整。问题依然很多。这也映射出自己学习不仔细,编程依然马虎。很多问题都想的不周全。这也进一步说明自己基础不扎实。

我相信我这里肯定还有不少问题,希望大家有时间给小弟看看,与此同时我也会不断改进,把这个做到自己的极限。