0x0C-开始行动

写在最前方

  • 对于线程的概念以及意义,我说一些
    • 现在我只说,一个多线程的技术对于C语言程序而言就像,原本只有你一个人在干活,现在突然有许多人愿意追随你,变成了以你为核心的多人合作模式,共同完成同一个任务
    • 导致的结果就是,这个任务被切分成多个模块,有可能很多人一起做同一个模块,有可能某些模块只有一个人在做,这就带来了什么问题呢?
    • “我” 对这个任务的掌控力度变低了,试想本来就是我一个人在开发这个程序,程序的每部分都是我一个字符一个字符敲上去的,自然了解程序的每个部分。但是现在,突然多了一些人来和你一起完成这个任务,必然导致有一部分程序代码不是由你亲自写下去的,虽然你指示他们怎么做,做出什么样的功能。
    • 但是毕竟不是你亲自写的,所以他们在协调工作的时候,很大可能性会出现问题,这个问题在多线程变成里面就是资源争夺问题,也就是同一个内存块, 在同一时刻被多于一个的线程访问,那么该如何处理呢?
    • C语言到现在已经支持多线程(然而零零散散的,虽然C11标准支持多线程库)了,并没有办法实际用在编程里,我一般使用 各平台的 API 或者 Glib 进行多线程编程,前者是不需要配置环境直接上手,缺点就是无法跨平台(万恶的Windows)。后者则是需要配置环境,且这个库说实话的确很臃肿,对于我们即将写的这个备份程序有些杀鸡用牛刀。
  • 好吧,如果硬要说的官方一点那就是:

    • 应用程序,进程,线程的区别:
    • 应用程序是一组数据和指令。
    • 多进程就相当于启动了多个应用程序,彼此之间独立(虚拟内存)
    • 进程就是应用程序运行起来的名称,两者的区别在于进程是有状态的,即它会变。
    • 线程和进程十分相似,都有状态,但是它的状态比进程少,并且线程之间可以十分轻易的共享数据,而这点是多进程无法比拟的(进程间共享数据开销极大)。
    • 状态指的就是程序运行起来产生的一些值,因为是运行后产生的所以形象的叫他们状态,例如寄存器里的值,栈(而不是堆,线程并没有自己的堆)的值
  • 看完上面的解释,就准备开始着手写程序了

写在中间

  • 首先我使用的是 Visual Studio 2013作为编译器
  • 创建 Win32控制台应用程序
  • 记得创建C源文件
  • 如果不小心点成C++也没事,改成.c就行。
  1. 创建一个入口源文件Entery.c,用于放置main函数

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. int main(int argc, char* argv[])
    4. {
    5. system("pause"); /* 因为Windows的命令行命令对大小写不敏感,所以命令无所谓大小写,从环境变量的配置就能知道这一点 */
    6. return 0;
    7. }
    8. 因为等一下需要构造一个字符显示界面模拟GUI的功能,故添加`#include <stdlib.h>`
    9. 当然并不是只因为这个原因。但是现在是为了使用其中的`system()`函数调用系统命令。
  • 好,那么接下来…
  • 等一下,要直接写程序了吗?程序功能呢?程序结构呢?从哪里开始写呢?

    • 这些都没有考虑啊!!
  • 所以,停下来我们好好构思构思!

    • 问题:
      1. 我们要做什么? : 一个多线程的备份程序
      2. 我们要怎么做? : 这个问题有点大,应该拆开
      3. 我们从哪里开始做? : 这个问题比较好回答。
    • 回答:

      1. 我们要实现把某个路径备份到某个特定路径
        • 首先要实现路径的设置,也就是说由键盘输入路径
        • 得到了路径之后,要得到该路径下所有文件夹和文件信息(在*nix下这俩玩意儿都是一个东西)
        • 行了就这么简单
      2. 先从输出所有文件和文件夹的结构开始
    • 问题:

      1. 怎么得到文件和文件夹的信息?
    • 回答:

      1. MSDN放在网上,虽然现在不提供离线版本了,但是也没有被墙啊。所以不懂的时候就一个字,查API文档!对于十分基本的API, MSDN甚至会给出示例代码。恰好,这就是一个。
  • 题外话

    • 其实会写这个程序没什么大不了,重要的是学习能力,一个自我学习的能力,就比如当我不知道一个东西,也无从下手的情况下,我应该进行联想,联想可能与他相关的各种方面,并且亲自去查,不厌其烦的查,遍地撒网,重点捞鱼。
  • 首先我们需要实现一个功能,就是遍历输出路径下的所有文件夹和文件

  • 在这之前,我们先设计一下界面,稍后…3, 2, 1,出现!

    1. while(1)
    2. {
    3. system("cls"); /* 系统的清屏命令 */
    4. do{
    5. puts("-------------------------------------------------");
    6. puts("-------------------------------------------------");
    7. puts("That is a System Back Up Software for Windows! ");
    8. puts("List of the software function : ");
    9. puts("1. Back Up ");
    10. puts("2. Set Back Up TO-PATH ");
    11. puts("3. Read Me ");
    12. puts("4. Exit ");
    13. puts("-------------------------------------------------");
    14. puts("Your Select: ");
    15. fscanf(stdin, "%d", &select);
    16. getchar(); /* 读取上方 fscanf 留在流里面的换行符 '\n' */
    17. }while ((select < 1) || (select > 4)); /* 如果选择无效 */
    18. system("cls");
    19. switch(select)
    20. {
    21. case 1 :
    22. break;
    23. case 2 :
    24. break;
    25. case 3 :
    26. break;
    27. case 4 :
    28. exit(0); /* 退出程序 */
    29. default :
    30. break;
    31. }/** switch(select) **/
    32. }/** while(1) **/
    33. system("pause");
    34. return 0;

    突然出现的这一段代码就是设计的界面,其实很简单,看看就懂了,不再多说。

    英文莫怪。

  • 紧接着,我们来实现第一个功能,显示结构,让我们吧这个功能函数叫做show_structure

    • 新建头文件showFiles.h

      1. /*头文件包裹一定要切记*/
      2. #ifndef INCLUDE_SHOWFILES_H
      3. #define INCLUDE_SHOWFILES_H
      4. /* 代码写在里面,这样就不会发生重定义,也能节省资源 */
      5. #endif
    • 新建源文件showFiles.c

      1. - 记得包含头文件`#include <showFiles.h>`
    • showFiles.h 稍后进行解释

      1. #include <stdio.h>
      2. #include <stdlib.h>
      3. #include <Windows.h> /* Windows API */
      4. #include <string.h> /* 字符串函数 */
      5. #include <tchar.h> /* _ftprintf */
      6. #include <setjmp.h> /* setjmp, longjmp */
      7. #define TRY_TIMES 3 /* 重新尝试获取的最大次数 */
      8. #define MIN_PATH_NAME _MAX_PATH /* 最小的限制 */
      9. #define LARGEST_PATH_NAME 32767 /* 路径的最大限制 */
      10. /* 我们需要在这里面包含函数的声明 */
      11. /** 加上文档注释,不太喜欢死板的硬套,选择自己觉得重要的信息记录吧
      12. * @version 1.0 2015/09/28
      13. * @author wushengxin
      14. * @param from_dir_name 源目录,即用于扫描的目录
      15. depth 递归的深度,用于在屏幕上格式化输出,每次增加 4
      16. * @function 用于输出显示目录结构的,可以改变输出流
      17. */
      18. void show_structure(const char * from_dir_name, int depth);

      题外话,在Visual Studio中,会强制要求你使用他们编写的安全函数,例如fscanf_s,如果你不想用的话,那就将它关闭吧,具体怎么操作,就当是一个小问题留给你自己。

    • showFiles.c 先不写太多,这里比较重要的是写法

      1. /* 首先是需要的一系列变量 */
      2. int times = 0; /* 用来配合 setjmp和longjmp重新获取句柄HANDLE的 */
      3. /** 操作时获取文件夹,文件信息的的必要变量 **/
      4. HANDLE file_handle;
      5. WIN32_FIND_DATAA file_data;
      6. LARGE_INTEGER file_size;

      file_handle : 文件的句柄,后期操作的主要对象

      file_data : 文件的信息,各种属性

      file_size : 文件的大小有可能非常大,需要使用特定的结构体保存

  • 到这里我们停下来,因为下一步我们要去实现获取路径的操作。

    • 问题:
      1. 我们要怎么样获取路径
      2. 我们获取到的路径要怎么存储
      3. 存储的路径要符合什么格式
    • 回答:

      1. 有两个路径:备份的来源路径,备份的目的路径。前者用键盘输入,后者在程序内部首先指定一个。
      2. 这里有两种方案:用系统栈存,用堆存
        • 前者是方便的内存管理,完全不用程序员操心,但是栈的大小比较小,一般只有几兆而已,这也是为什么递归容易爆栈的原因。速度较快
        • 后者是近似巨大的内存上限,对于32位系统的Windows应用程序而言,可以有2~3GB的分配空间(4GT机制),64位就更为可怕了(Windows8 最大有512GB, Windows7 最大有 192GB,服务器系列大概是1~2TB)。速度较慢
        • 不熟练的程序员应该尽可能选择前者。
        • 这里采用后者,前者的代码也会一并附上。
      3. 对于微软API处理自己的Windows路径,一般要求末尾以/或者\结尾,前者在C语言中不是转义,所以比较好存储,如果需要使用后者,可以选择如此\\就行了。

        1. char dir_path_1[PATH_MAX] = "../";
        2. char dir_path_2[PATH_MAX] = "..\\";
        3. /* 两种效果一致,且占的空间也相同 */
    • 注意
      • 这里有涉及到一个问题,那就是Windows下的路径限制,在windef.h中定义了一个常量PATH_MAX的值为260,也就是说最大的路径长路为260字节,但是如果我们的路径名超过了这个长度怎么办呢?
      • 这里直接给出了解决办法,就是添加前缀\\?\,这样长度限制就增加到了32767
      • 此处不予以实现,日后遇见情况的话可以当作一个解决的办法。
  • 解决了上一个关于路径的问题,我们就需要考虑一下如何设计实现这个功能,首先要达到模块化的目的,即尽量减少每个函数的功能。

    • 问题
      • 都需要什么功能?
    • 回答
      • 一个主要的函数用来递归(也可以用循环,循环的好处就也在于不容易爆栈)
      • 一个用来专门给路径分配空间的函数
      • 一个用来释放分配空间的函数
      • 一个对输入的路径进行处理的函数,让路径变得规范
  • showFiles.h 变个魔术 3, 2, 1,添加了如下代码

    1. /**
    2. * @version 1.0 2015/09/28
    3. * @author wushengxin
    4. * @param src 外部传进来的,用于向所分配空间中填充的路径
    5. * @function 用于在堆上为存储的路径分配空间。
    6. */
    7. char * make_path(const char * src);
    8. /*
    9. * @version 1.0 2015/09/28
    10. * @author wushengxin
    11. * @param src 外部传进来的,是由 make_path 所分配的空间,将其释放
    12. * @function 用于释放 make_path 分配的内存空间
    13. */
    14. void rele_path(char * src);
    15. /*
    16. * @version 1.0 2015/09/28
    17. * @author wushengxin
    18. * @param src_path 用于 FindFirstFile()
    19. src_file 用于添加找到的目录名,形成新的目录路径
    20. * @function 用于调整用户从键盘输入的路径字符串,使他们变得一致,便于处理
    21. */
    22. void adjust_path(char * __restrict src_path, char * __restrict src_file);
    23. /*
    24. * @version 1.0 2015/09/28
    25. * @author wushengxin
    26. * @param src 外部传入的,用于调整
    27. * @function 用于替换路径中的 / 为 \ 的
    28. */
    29. void repl_str(char * src);

    具体功能在文档里已经写的很清楚了,唯一要解释的就是最后两个函数,本来是一体的,后来被我拆开成了两个函数,为了也是功能更加清晰

    倒数第二个函数 adjust_path 的作用是将路径处理成符合 Windows 函数 FindFirstFile要求.aspx)可以具体看看。

  • showFiles.c 继续 show_structure 的实现

    • shows_tructure

      1. size_t length = strlen(from_dir_name);
      2. char * dir_buf = make_path(from_dir_name); //路径
      3. char * dir_file_buf = make_path(from_dir_name); //文件
      4. if (dir_buf == NULL || dir_file_buf == NULL)
      5. return; /* 如果分配失败就结束函数 */
      6. adjust_path(dir_buf, dir_file_buf); /* 调整路径和文件格式到标准格式 */
      7. repl_str(dir_buf);
      8. repl_str(dir_file_buf);

      这是调用 WINDOWS API 之前的所有操作,来一一实现他们

      首先是分配空间给路径

    • make_path : 24 lines·

      对于这个函数的功能便是,为需要存储的路径分配空间。

      1. int times = 0;
      2. size_t len_of_src = strlen(src); /* 需要分配的长度 */
      3. size_t len_of_dst = MIN_PATH_NAME;
      4. if (len_of_src > MIN_PATH_NAME - 10) /* \\?\ //* 8个字符 */
      5. { /* 这里用了10这个神奇的垃圾数,所以必须做一点注释,以防忘记 */
      6. len_of_dst = LARGEST_PATH_NAME;
      7. if (len_of_src > LARGEST_PATH_NAME - 10)
      8. {
      9. fprintf(stderr, "The Path Name is larger than 32767, Which is not Support!\n%s", src);
      10. return NULL;
      11. }
      12. }
      13. setjmp(alloc_jmp); /* alloc_jmp to here */
      14. char * loc_buf = malloc(len_of_dst + 1);
      15. if (loc_buf == NULL)
      16. {
      17. fprintf(stderr, "ERROR OCCUR When malloc the memory at %s\n Try the %d th times", __LINE__, times+1);
      18. if (times++ < TRY_TIMES)
      19. longjmp(alloc_jmp, 0); /* alloc_jmp from here */
      20. return NULL;
      21. }
      22. //sprintf(loc_buf, "\\\\?\\%s", src); /* 作为日后的扩展 */
      23. strcpy(loc_buf, src);
      24. return loc_buf;

      对于 10 这个数的考虑是,至少留出 8 个空位给所说的字符,加 2 凑整。

      对于函数 malloc ,在这里没有进行包裹,是因为这只是一个预热的功能,后期在实现备份的时候,会对它进行包装,也使得错误处理的代码隐藏,让函数功能更加清晰。

    • adjust_path : 16 lines

      其次是调整路径的函数,功能就是调整路径

      1. size_t length = strlen(src_path); /* 两个参数的长度在此函数调用之前必定一致 */
      2. if (length == 1) /* 处理情况为,当用户输入的是根目录的情况 例如: C */
      3. {
      4. strcat(src_file, ":/");
      5. }
      6. else if (src_path[length - 1] != '\\' && src_path[length - 1] != '/')
      7. {
      8. strcat(src_file, "/");
      9. }
      10. else
      11. {
      12. src_path[length - 1] = '/';
      13. }
      14. strcpy(src_path, src_file);
      15. strcat(src_path, "/*");
      16. return;

      当用户输入的是一个字符的根目录,我们要将其处理为 C:/ 这样的形式

      当用户输入的是不带/结尾的,我们需要将其添加上 /

      当用户输入以 \ 结尾的路径时,将其替换为 /,虽然后方又全部换成了 \

      将目录处理为带 /* 结尾的,以达到 API 的要求

      src_file 用于将目录下的子目录名连接。生成新的目录。 src_path 用于递交给 API 扫描目录下的所有文件和文件夹。

    • repl_str : 7 lines

      1. size_t length = strlen(src);
      2. for (size_t i = 0; i <= length; ++i)
      3. {
      4. if (src[i] == '/')
      5. src[i] = '\\';
      6. }
      7. return;

      不再赘述这个函数的功能

      到此处,所有在第一个 Windows API 之前调用的函数都实现了,接下来要做什么?

  • 当然是调用API函数啦

    • show_structure

      1. /* 开始调用 Windows API 获取路径下的文件和文件夹信息 */
      2. setjmp(get_hd_jmp);
      3. fileHandle = FindFirstFileA(dir_buf, &fileData);
      4. if (fileHandle == INVALID_HANDLE_VALUE) /* 如果无法获取句柄超过上限次数,就退出 */
      5. {
      6. fprintf(stderr, "The Handle getting Failure! \n");
      7. if (times++ < TRY_TIMES)
      8. longjmp(get_hd_jmp, 0);
      9. return;
      10. }

      对于这一段代码的解释,其实核心就是第二句代码,其中的函数 FindFirstFileA需要解释一下。

      Windows API 文档 MSDN 中介绍的是 FindFirstFile ,但是某些情况下(定义了UNICODE宏,不知道有没有记错),这个官方提供的接口会被定义(#define)成 FindFirstFileW,如果使用 char *ANSI 字符串当成参数的话是会获取句柄失败的!并且另一个参数使用的 file_data 类型也是 ANSIWIN32_FIND_DATAA

      所以这里显式地选择调用 FindFirstFileA 而不是让 Windows 帮我们选择。

      接下来我们要做的事情就是,遍历这个目录下的所有文件和文件夹,提取出来他们的信息:

      1. do{
      2. char * tmp_dir_file_buf = make_path(dir_file_buf);
      3. if (fileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
      4. { /* 如果是文件夹 */
      5. fprintf(stderr, "%*s%s\t<DIR>\t\n", depth, "", fileData.cFileName);
      6. if (strcmp(fileData.cFileName, ".") == 0 || /* . 和 .. 便是当前文件夹和上一级文件夹, 世界上所有系统都一样 */
      7. strcmp(fileData.cFileName, "..") == 0)
      8. continue;
      9. strcat(tmp_dir_file_buf, fileData.cFileName); /* 将文件名连接到当前文件夹路径之后,形成文件路径 */
      10. show_structure(tmp_dir_file_buf, depth + 4);
      11. }
      12. else
      13. {
      14. fileSize.LowPart = fileData.nFileSizeLow; /* 输出大小 */
      15. fileSize.HighPart = fileData.nFileSizeHigh;
      16. fprintf(stderr, "%*s%s \t%ld bytes\t\n", depth, "", fileData.cFileName,
      17. fileSize.QuadPart);
      18. }
      19. rele_path(tmp_dir_file_buf); /* 这个的实现稍后放出 */
      20. } while (FindNextFileA(fileHandle, &fileData));

      代码是仿照 MSDN 提供的 官方例子.aspx)改写的。

      其中第五句代码,用到了前方提到过的格式化输出的一个不常用的技巧,占位,忘记的可以回去看看在第一章

      采用的方法是递归,循环的方法留给看者自己实现,思路很简单,用一个队列或者栈存放所有找到的目录和文件,依次取出直到栈或者队列为空。

      以及最后一段的代码,用于收尾:

      1. FindClose(fileHandle);
      2. rele_path(dir_buf);
      3. rele_path(dir_file_buf);
      4. return;
    • rele_path : 4 lines

      1. free(src);
      2. src = NULL;
      3. return;
  • 最后在 Entry.cmain 函数中,在 switchcase 1 标签范围内,加上一些获取和处理输入的函数 : (因为这里只会使用一次,故采用的是系统栈而不是在堆上分配)

    1. char tmp[_MAX_PATH];
    2. ...
    3. case 1 :
    4. scanf("%s", tmp);
    5. printf("Enter : %s\n", tmp);
    6. getchar(); /* 前方提到过,作用是清理标准输入流 */
    7. show_structure(tmp, 0);
    8. system("pause");
    9. break;

现在编译运行

  • 成功了!对自己的代码很有信心,嗯!
  • 中文路径也是可以的,别怕
  • 对于超过260字符的路径没有测试,大概能猜到是不行的。但是解决方案上方也有提到。
  • 这就是预热的函数,比较详细,后方代码就不会如此赘述,而是更加简洁干练的上代码和解释

写在最后面

  • 总结一个词,代码横陈,但是逻辑还算清晰
  • 只是一个预热的作用,正片代码只是为了提前让思路更加清晰,且能测试出Windows API的某些潜在缺陷以及要求。
  • 下面将开始真正的进入功能的编写。