zoukankan      html  css  js  c++  java
  • TLPI读书笔记第18章-目录与链接2

    18.8 读目录: opendir()和 readdir()

    本节所述库函数可用于打开一个目录,并逐一获取其包含文件的名称。

    #include<dirent.h>
    DIR *opendir(const char *dirpath);
    DIR *fdopendir(int fd);
    struct dirent *readdir(DIR *dirp);

    opendir()函数打开一个目录,并返回指向该目录的句柄,供后续调用使用。 opendir()函数打开由 dirpath 指定的目录,并返回指向 DIR 类型结构的指针。该结构即所谓目录流( directory stream),亦即调用者传递给下述其他函数的句柄。一旦从 opendir()返回,则将目录流指向目录列表的首条记录。 除了要创建的目录流所针对的目录由打开文件描述符指代之外, fdopendir()与 opendir()并无不同。

    提供 fdopendir()函数,意在帮助应用程序免受 18.11 节所述各种竞态条件的困扰。调用 fdopendir()成功后,文件描述符将处于系统的控制之下,且除了利用本节余下部分所描述的函数之外,程序不应采取任何其他方式对其进行访问。

    struct dirent{
       ino_t d_ino;/*i-node number*/
       char d_name[];/*文件名*/
    }

    readdir()函数从一个目录流中读取连续的条目。每调用 readdir()一次,就会从 dirp 所指代的目录流中读取下一目录条目,并返回一枚指针,指向经静态分配而得的 dirent 类型结构,每次调用 readdir()都会覆盖该结构。

    调用 lstat()(或者 stat(),如果应对符号链接解引用时)可获得 d_name 所指向文件的更多信息,其中,路径名由之前调用 opendir()时指定的 dirpath 参数与“/”字符以及 d_name 字段的返回值拼接组成。 readdir()返回时并未对文件名进行排序,而是按照文件在目录中出现的天然次序(这取决于文件系统向目录添加文件时所遵循的次序,及其在删除文件后对目录列表中空隙的填补方式)。 (命令 ls–f 对文件列表的排列与调用 readdir()时一样,均未做排序处理。 )

    一旦遇到目录结尾或是出错, readdir()将返回 NULL,针对后一种情况,还会设置 errno 以示具体错误。为了区别这两种情况,可编码如下:

    errno=0;
    direntp=readdir(dirp);
    if(direntp==NULL){
       if(errno!=0){
           /*handle error*/
      }else{
           /*reach end of dir*/
      }
    }

    如果目录内容恰逢应用调用 readdir()扫描该目录时发生变化,那么应用程序可能无法观察到这些变动。 SUSv3 明确指出,对于 readdir()是否会返回自上次调用 opendir()或 rewinddir()后在目录中增减的文件,规范不做要求。至于最后一次执行上述调用前就存在的文件,应确保其全部返回。

    rewinddir()函数可将目录流回移到起点,以便对 readdir()的下一次调用将从目录的第一个文件开始。

    closedir()函数将由 dirp 指代、处于打开状态的目录流关闭,同时释放流所使用的资源。

    #include<dirent.h>
    void rewinddir(DIR *dirp);
    int closedir(DIR *dirp);

    SUSv3 还定义了两个高级函数: telldir()和 seekdir(),允许随机访问目录流。

    目录流与文件描述符

    有一个目录流,就有一个文件描述符与之关联。 dirfd()函数返回与 dirp 目录流相关联的文件描述符。

    #include<dirent.h>
    int dirfd(DIR *dirp);

    例如,将 dirfd()返回的文件描述符传递给 fchdir()(参见 18.10 节),就可以把进程的当前工作目录改成相应目录。此外,还可以将其传递给 18.11 节所述各函数的 dirfd 参数。 dirfd()函数还见诸于 BSD 系统,但在其他实现中则鲜有踪迹。该函数未获 SUSv3 接纳,但 SUSv4 则对其做了规范。 这里值得一提的是, opendir()会为与目录流相关联的文件描述符自动设置 close-on-exec 标志FD_CLOEXEC,以确保当执行 exec()时自动关闭该文件描述符。 ( SUSv3 要求这一行为。 )

    readdir_r()函数

    readdir_r()函数是 readdir()的变体。二者之间语义上的关键差异在于前者是可重入的,而后者不是。这是因为 readdir_r()对文件条目的返回利用的是由调用者分配的 entry 参数,而 readdir()则是将信息置于静态分配的结构并返回其指针。 21.1.2 节和 31.1 节讨论了可重入性(reentrancy)。

    #include<dirent.h>
    int readdir_r(DIR *dirp,struct dirent *entry,struct dirent **result);

    针对既定 dirp,亦即之前调用 opendir()所打开的目录流, readdir_r()将下一项目录条目置于由 entry 指向的 dirent 结构中。另外,还会在 result1中放置指向该结构的一枚指针。如果抵达目录流尾部, 那么会在 result2中返回 NULL(且 readdir_r()返回 0)。 当出现错误时, readdir_r()不会返回-1,而是返回一个对应于 errno 的正整型值。 在 Linux 中, dirent 结构的 d_name 字段是大小为 256 字节的一个数组,足以容纳可能出现的最长文件名。虽然有几个其他的 UNIX 实现也为 d_name 定义了相同的大小,但 SUSv3对此却并做规定,而另一些 UNIX 实现则将该字段定义为 1 字节的数组,并将正确分配结构大小的工作交给调用程序。这时,应将 d_name 字段大小设定为常量 NAME_MAX+1(考虑终止空字节)。为确保可移植性,应用程序应以如下方式分配 dirent 结构:

    鉴于 dirent 结构中 d_name 字段(该属性总是位于结构的最后)之前各类属性的数量和大小在不同系统中实现不一,采用 offsetof()宏(定义于<stddef.h>中)可避免程序对此产生依赖

    18.9 文件树遍历: nftw()

    nftw()函数允许程序对整个目录子树进行递归遍历,并为子树中的每个文件执行某些操作(即,调用由程序员定义的函数)

    nftw()函数遍历由 dirpath 指定的目录树, 并为目录树中的每个文件调用一次由程序员定义的 func 函数。

    #define _XOPEN_SOURCE 500
    #include<ftw.h>
    int nftw(const char *dirpath,
           int (*func)(const char *pathname,const struct stat *statbuf,int typeflag,struct FTW *ftwbuf),
           int nopenfd,int flags);

    /*func是一个自定义函数*/

    默认情况下, nftw()会针对给定的树执行未排序的前序遍历,即对各目录的处理要先于各目录下的文件和子目录。 当 nftw()遍历目录树时,最多会为树的每一层级打开一个文件描述符。参数 nopenfd 指定了 nftw()可使用文件描述符数量的最大值。如果目录树深度超过这一最大值,那么 nftw()会在做好记录的前提下,关闭并重新打开描述符,从而避免同时持有的描述符数目突破上限 nopenfd(从而导致运行越来越慢)。

    在较老的 UNIX 实现中,有的系统要求每个进程可打开的文件描述符数量不得超过 20 个,这更突显出这一参数的必要性。现代 UNIX 实现允许进程打开大量的文件描述符,因此,在指定该数目时出手可以大方一些(比如, 10 或者更多)。

    nftw()的 flags 参数由 0 个或多个下列常量相或(|)组成,这些常量可对函数的操作做出修正。影响遍历行为

     

    参数列表1:flags参数

    FTW_CHDIR 在处理目录内容之前先调用 chdir()进入每个目录。如果打算让 func 在 pathname 参数所指定文件的驻留目录下展开某些工作,那么就应当使用这一标志。

    FTW_DEPTH 对目录树执行后序遍历。这意味着, nftw()会在对目录本身执行 func 之前先对目录中的所有文件(及子目录)执行 func 调用。 (这一标志名称容易引起误会—nftw()遍历目录树遵循的是深度优先原则,而非广度优先。而这一标志的作用其实就是将先序遍历改为后序遍历。 )

    FTW_MOUNT 不会越界进入另一文件系统。因此,如果树中某一子目录是挂载点,那么不会对其进行遍历。

    FTW_PHYS 默认情况下, nftw()对符号链接进行解引用操作。而使用该标志则告知 nftw()函数不要这么做。相反,函数会将符号链接传递给 func 函数,并将 typeflag 值置为 FTW_SL,如下所述。 nftw()为每个文件调用 func 时传递 4 个参数。第一个参数 pathname 是文件的路径名。这个路径名可以是绝对路径,也可以是相对路径。如果指定 dirpath 时使用的是绝对路径,那么pathname 就可能是绝对路径。反之,如果指定 dirpath 时使用的是相对路径名,则 pathname中的路径可能是相对于进程调用 ntfw()时的当前工作目录而言。第二个参数 statbuf 是一枚指针,指向 stat 结构(参见 15.1 节),内含该文件的相关信息。

    第三个参数 typeflag 提供了有关该文件的深入信息,并具有如下特征值之一。是系统返回给自定义函数的特征参数

    参数列表2:typeflag

    FTW_D 这是一个目录。

    FTW_DNR 这是一个不能读取的目录(所以 nftw()不能遍历其后代)。

    FTW_DP 正在对一个目录进行后序遍历,当前项是一个目录,其所包含的文件和子目录已经处理完毕。

    FTW_F 该文件的类型是除目录和符号链接以外的任何类型。

    FTW_NS 对该文件调用 stat()失败,可能是因为权限限制。 Statbuf 中的值未定义。

    FTW_SL 这是一个符号链接。仅当使用 FTW_PHYS 标志调用 nftw()函数时才返回该值。

    FTW_SLN 这是一个悬空的符号链接。 仅当未在 flags 参数中指定 FTW_PHYS 标志时才会出现该值。 Func 的第四个参数 ftwbuf 是一枚指针,所指向结构定义如下:

    struct FTW{
       int base;
       int level;
    };

    该结构的 base 字段是指 func 函数中 pathname 参数内文件名部分(最后一个“ /”字符后的部分)的整型偏移量。 level 字段是指该条目相对于遍历起点(其 level 为 0)的深度。 每次调用 func 都必须返回一个整型值,由 nftw()加以解释。如果返回 0, nftw()会继续对树进行遍历,如果所有对 func 的调用均返回 0,那么 nftw()本身也将返回 0 给调用者。若返回非 0 值,则通知 nftw()立即停止对树的遍历,这时 nftw()也会返回相同的非 0 值。 由于 nftw()使用的数据结构是动态分配的,故而应用程序提前终止目录树遍历的唯一方法就是让 func 调用返回一个非 0 值。调用 longjmp()( 6.8 节)从 func 退出会导致不可预期的结果—至少会引起内存泄漏。

    示例程序

    程序清单 18-3 展示了 nftw()的使用。

    程序清单 18-3 中程序以层级缩进方式显示了一个目录树中的文件。每行显示一个文件,内容包括文件名、文件类型及 i-node 编号。可通过命令行选项来指定 nftw()调用中的 flags 参数值。下面的 shell 会话展示了运行程序的示例结果。首先创建一个新的空目录,并在其中填充各种类型的文件。 然后使用该程序调用 nftw()函数,其 flags 参数为 0: 从以上输出可见,对符号链接 s1 进行了解析。

    然后再使用该程序来调用 nftw()函数,令 flags 参数包含 FTW_PHYS 和 FTW_DEPTH 标志: 从以上输出可见,未对符号链接 s1 进行解析。

    nftw()的 FTW_ACTIONRETVAL 标识

    始于 2.3.3 版本, glibc 允许在 ntfw()的 flags 参数中指定一个额外的非标准标志 FTW_ACTIONRETVAL。此标志改变了 nftw()函数对 func()返回值的解释方式。当指定该标识时, func()应返回下列值之一。

    FTW_CONTINUE 与传统 func()函数返回 0 时一样,继续处理目录树中的条目。

    FTW_SKIP_SIBLINGS. 不再进一步处理当前目录中的条目,恢复对父目录的处理。

    FTW_SKIP_SUBTREE 如果 pathname 是目录(即 typeflag 为 FTW_D),那么就不对该目录下的条目调用 func()。恢 复进行对该目录的下一个同级目录的处理。

    FTW_STOP 与传统 func()函数返回非 0 值时一样,不再进一步处理目录树下的任何条目。 nftw()将返回 FTW_STOP 给调用者。

    想从<ftw.h>文件中获得对 FTW_ACTIONRETVAL 的定义,必须定义_GNU_SOURCE 特性测试宏。

    18.10 进程的当前工作目录

    一个进程的当前工作目录( current working directory)定义了该进程解析相对路径名的起点。新进程的当前工作目录继承自其父进程。

    获取当前工作目录

    进程可使用 getcwd()来获取当前工作目录。

    #include<unistd.h>
    char *getcwd(char *cwdbuf,size_t size);

    getcwd()函数将内含当前工作目录绝对路径的字符串(包括结尾空字符)置于 cwdbuf 指向的已分配缓冲区中。调用者必须为 cwdbuf 缓冲区分配至少 sizeg 个字节的空间。通常,cwdbuf 的大小与 PATH_MAX 常量相当。 一旦调用成功, getcwd()将返回一枚指向 cwdbuf 的指针。如果当前工作目录的路径名长度超过 size 个字节,那么 getcwd()会返回 NULL,并将 errno 置为 ERANGE。 在 Linux/x86-32 系统中, getcwd()返回指针所指向的字符串最大长度可达 4096 个字节。 如果当前工作目录(以及 cwdbuf 和 size)突破了这一限制,那么就会直接对路径名做截断处理,移去始于起点的整个目录前缀(字符串仍以空字符结尾)。换言之,当当前工作目录的绝对路径超出这一限制时, getcwd()的行为也不再可靠。 实际上, Linux 的 getcwd()系统调用为要返回的路径名在内部分配了一个虚拟内存页。x86-32 架构的页大小为 4096 字节,而在页尺寸更大的架构中(比如, Alpha 的页大小为 8192字节), getcwd()能返回更长的路径名。 若 cwdbuf 为 NULL,且 size 为 0,则 glibc 封装函数会为 getcwd()按需分配一个缓冲区,并将指向该缓冲区的指针作为函数的返回值。为避免内存泄漏,调用者之后必须调用 free()来释放这一缓冲区。对可移植性有所要求的应用程序应当避免依赖该特性。大多数其他实现则针对 SUSv3 规范提供了一个更为简单的扩展。如果 cwdbuf 是 NULL,那么 getcwd()将分配一个大小为 size 字节的缓冲区,用于向调用者返回结果。

    glibc 的 getcwd()也实现了这一特性。

    只要具有合适的权限(大体要求是,身为进程属主或者具有 CAP_SYS_PTRACE 能力),就可通过读取( readlink()) Linux 专有符号链接/proc/PID/cwd 的内容来确定任何进程的当前工作目录。

    改变当前工作目录
    #include<unistd.h>
    int chdir(const char *pathname);
    int fchdir(int fd);

    chdir()系统调用将调用进程的当前工作目录改变为由 pathname 指定的相对或绝对路径名(如属于符号链接,还会对其解除引用)。 fchdir()系统调用与 chdir()作用相同,只是在指定目录时使用了文件描述符,而该描述符是之前调用 open()打开相应目录时获得的。

    以下代码片段所示为使用 fchdir()将进程的当前工作目录变为另一位置,然后再改回原始位置:

    int fd;
    fd=open('.',O_RDONLY);
    chdir(somepath);
    fchdir(fd);
    close(fd);

    使用 chdir()达到同等效果的代码如下所示:

    char buf[PATH_MAX];
    get_cwd(buf,PATH_MAX);
    chdir(somepath);
    chdir(buf);

     

    18.11 针对目录文件描述符的相关操作

    始于版本 2.6.16, Linux 内核提供了一系列新的系统调用,在执行与传统系统调用相似任务的同时,还提供了一些附加功能,对某些应用程序非常有用。表 18-2 对这些调用进行了归纳。之所以在本章介绍这些系统调用,是因为它们对进程当前工作目录的传统语义做了改动。

    为便于描述这些系统调用,这里就以 openat()为例。

    #define _XOPEN_SOURCE 700
    #include<fcntl.h>
    int openat(int dirfd,const char *pathname,int flags,...);

    openat()系统调用类似于传统的 open()系统调用, 只是添加了一个 dirfd 参数, 其作用如下。 1.如果 pathname 中为一相对路径名,那么对其解释则以打开文件描述符 dirfd 所指向的目录为参照点,而非进程的当前工作目录。 2.如果 pathname 中为一相对路径,且 dirfd 中所含为特殊值 AT_FDCWD,那么对 pathname的解释则相对与进程当前工作目录(即与 open(2)行为一致)而言。 3.如果 pathname 中为绝对路径,那么将忽略 dirfd 参数。 openat()的 flag 参数目的与 open()相同。然而,部分表 18-2 中所列系统调用还支持 flags 参数,这是相应的传统系统调用所不具备的,其目的在于修改调用语义。出现频率最高的标志为AT_SYMLINK_NOFOLLOW,其含义是如果 pathname 为符号链接,那么系统调用将操作于符号链接本身,而非符号链接所指向的文件。(linkat()系统调用提供了 AT_SYMLINK_FOLLOW 标志, 其作用正好相反,即改变 linkat()的默认行为,当 oldpath 属于符号链接时对其进行解引用操作。 )

    之所以要支持表 18-2 中所列的系统调用,其原因有二(此处再以 openat()为例)。 1.当调用 open()打开位于当前工作目录之外的文件时,可能会发生某些竞态条件。而使用 openat()就能够避免这一问题。在调用 open()的同时,如果 pathname 目录前缀的某些部分发生了改变,就可能导致竞争。要想避免这类竞态,可以针对目标目录打开一个文件描述符,然后将该描述符传递给 openat()。 2.如第 29 章所述,工作目录是进程的属性之一,为进程中所有线程所共享。而对某些应用程序而言,需要针对不同线程拥有不同的“虚拟”工作目录。将 openat()与应用所维护的目录文件描述符相结合,就可以模拟出这一功能。

    为了获得对这些系统调用的声明,必须在包含相应头文件之前(比如定义 open()的<fcntl.h>)将_XOPEN_SOURCE 特性测试宏定义为大于或等于 700 的值。 另外, 将_POSIX_C_SOURCE 宏的值定义为大于或等于 200809 也能收到同样效果。

    18.12 改变进程的根目录: chroot()

    每个进程都有一个根目录,该目录是解释绝对路径(即那些以/开始的目录)时的起点。 默认情况下,这是文件系统的真实根目录。 (新进程从其父进程处继承根目录。 )有些场合需要改变一个进程的根目录,而特权级( CAP_SYS_CHROOT)进程通过 chroot()系统调用能够做到这一点。

    #define _BSD_SOURCE
    #include<unistd.h>
    int chroot(const char *pathname);

    chroot()系统调用将进程的根目录改为由 pathname 指定的目录(如果 pathname 是符号链接,还将对其解引用)。自此,对所有绝对路径名的解释都将以该文件系统的这一位置作为起点。

    鉴于这会将应用程序限定于文件系统的特定区域,有时也将此称为设立了一个 chroot 监禁区。 chroot()还是获得了大多数 UNIX 实现的支持。

    ftp 程序就是应用 chroot()的典型实例之一。作为一种安全措施,当用户匿名登录 ftp 时, ftp程序将使用 chroot()为新进程设置根目录—一个专门预留给匿名登录用户的目录。调用chroot()后,用户将受困于文件系统中新根目录下的子树中,无法在整个文件系统中信马由缰。 (这里所依赖的事实是根目录是其自身的父目录。也就是说/..是/的一个链接,所以改变目录到/后再执行 cd ..命令时,用户依然会待在同一目录下。 )

    通常情况下,不是随便什么程序都可以在 chroot 监禁区中运行的,因为大多数程序与共享库之间采取的是动态链接的方式。因此,要么只能局限于运行静态链接程序,要么就在监禁区中复制一套标准的共享库系统目录(比如,包括/lib 和/usr/lib)(针对这一点, 14.9.4 节描述的绑定挂载特性就派上了用场)。 chroot()系统调用从未被视为一个完全安全的监禁机制。首先,特权级程序可以在随后对chroot()的进一步调用中利用种种手段而越狱成功。例如,特权级( CAP_MKNOD)程序能够使用 mknod()来创建一个内存设备文件(类似于/dev/mem),并通过该设备来访问 RAM 的内容,到那时,就一切皆有可能了。

    通常,最好不要在 chroot 监禁区文件系统内放置 set-user-ID-root程序。即便是对于无特权程序,也必须小心防范如下几条可能的越狱路线。 1.调用 chroot()并未改变进程的当前工作目录。因此,通常应在调用 chroot()之前或者之后调用一次 chdir()函数(例如, chroot()调用之后执行 chdir("/"))。如果没有这么做,那么进程就能够使用相对路径去访问监狱之外的文件和目录。(一些 BSD 的衍生系统杜绝了这一可能性—如果当前工作目录位于新的根目录树之外,那么 chroot()调用会将其修改为与根目录一致。 ) 2.如果进程针对监禁区之外的某一目录持有一打开文件描述符,那么结合 fchdir()和chroot()即可越狱成功,如下面代码所示:

    为了防止这种可能性,必须关闭所有指向监禁区外目录的文件描述符。 (其他一些 UNIX实现提供了 fchroot()系统调用,可用于获得与上述代码片段类似的结果。 ) 即使针对上述可能性采取了防范措施,仍不足以阻止任意非特权程序(即无法控制其操作的程序)越狱成功。遭到囚禁的进程仍然能够利用 UNIX 域套接字来接受(自另一进程处)指向监禁区外目录的文件描述符。 ( 61.13.3 节简要描述了进程间利用套接字来传递文件描述符的概念。 )将这一文件描述指定为 fchdir()调用的入参,程序 即可将其当前工作目录置于监禁区外,之后再通过相对路径来随意访问文件和目录。 一些 BSD 衍生系统提供的 jail()系统调用解决了包括上述问题在内的不少问题,其所创建的监禁区即使针对特权级进程也是安全的。

    18.13 解析路径名: realpath()

    realpath()库函数对 pathname(以空字符结尾的字符串)中的所有符号链接一一解除引用,并解析其中所有对/.和/..的引用,从而生成一个以空字符结尾的字符串,内含相应的绝对路径名

    #include<stdlib.h>
    char *realpath(const char *pathname,char *resolved_path)

    生成的字符串将置于 resolved_path 指向的缓冲区中,该字符串应当是一个字符数组,长度至少为 PATH_MAX 个字节。一旦调用成功, realpath()将返回指向该字符串的一枚指针。 glibc 的 realpath()实现允许调用者将 resolved_path 参数指定为空。这时, realpath()会为经解析生成的路径名分配一个多达 PATH_MAX 个字节的缓冲区, 并将指向该缓冲区的指针作为结果返回。(调用者必须自行调用free()来释放该缓冲区。) 程序清单 18-4 中的程序采用 readlink()和 realpath()来读取符号链接的内容, 并将该链接解析为一个绝对路径名。下面是运行该程序的一个示例:

    18.14 解析路径名字符串: dirname()和 basename()

    dirname()和 basename()函数将一个路径名字符串分解成目录和文件名两部分。 (这些函数 执行的任务与 dirname(1)和 basename(1)命令相类似。 )

    #include<libgen.h>
    char *dirname(char *pathname);
    char *basename(char *pathname);

    比如,给定路径名为/home/britta/prog.c, dirname()将返回/home/britta,而 basename()将返 回 prog.c。将 dirname()返回的字符串与一斜线字符( /)以及 basename()返回的字符串拼接起 来,将生成一条完整的路径名。 关于 dirname()和 basename()的操作请注意以下几点。 1.将忽略 pathname 中尾部的斜线字符。 2.如果 pathname 中未包含斜线字符,那么 dirname()将返回字符串.(点),而 basename()将 返回 pathname。 3.如果 pathname 仅由一个斜线字符组成,那么 dirname()和 basename()均将返回字符串/。将其应用于上述的拼接规则,所创建的路径名字符串为///。该路径名属于有效路径名。因为多个连续斜线字符相当于单个斜线字符,所以路径名///就相当于路径名/。 4.如果 pathname 为空指针或者空字符串,那么 dirname()和 basename()均将返回字符串.(点)。 (拼接这些字符串将生成路径名./.,对等于.,即当前目录。 )

    表 18-3 所示为 dirname()和 basename()针对各种示例路径名所返回的字符串。

    dirname()和 basename()均可修改 pathname 所指向的字符串。因此,如果希望保留原有的路径名字符串,那么就必须向 dirname()和 basename()传递该字符串的副本,如程序清单 18-5所示。该程序使用 strdup()(该函数调用了 malloc())来制作传递给 dirname()和 basename()的字符串副本,然后再使用 free()将其释放。

    最后需要指出的是, dirname()和 basename()所返回的指针均可指向经由静态分配的字符串,对相同函数的后续调用可能会修改这些字符串的内容

  • 相关阅读:
    ObjectiveC 日记⑦ 内存管理
    Jquery自定义分页插件
    C#中的静态类和静态成员
    多线程访问共同的代码或者对象:lock避免出错
    wordpress绑定新浪微博
    组态软件基础知识概述
    书籍推荐:《网站运营直通车:7天精通SEO》
    wordpress代码高亮插件推荐:AutoSyntaxHighlighter
    书籍推荐:《伟大是熬出来的:冯仑与年轻人闲话人生》
    wince平台用xml文件做配置文件
  • 原文地址:https://www.cnblogs.com/wangbin2188/p/14662960.html
Copyright © 2011-2022 走看看