天天看点

Gnu/Linux系统C编程之 - 系统与进程信息

这里主要介绍/proc伪文件系统及uname()函数来获取系统或进程的一些信息。

/proc文件系统介绍

在早期的UNIX发行版中,并不能很容易的分析内核的一些属性,并且很难回答以下问题:

系统有多少进程正在运行,并且谁拥有这些进程?

一个进程都打开了哪些文件?

哪些文件目前被锁住了,并且哪些进程拥有这些文件锁?

系统有哪些套接字正在使用?

一些早期的UNIX发行版解决该问题是允许有权限的程序进入内核的内存空间的数据结构中。这样的解决办法有诸多不便。然而,最蛋疼的是它需要程序员了解的内核中的数据结构,并且这些数据结构在不同的内核版本中会略有不同,需要程序员根据实际所依赖的内核进行代码的重写。

为了提供便捷的访问内核信息,很多现代的UNIX发行版提供了/proc虚拟文件系统。该文件系统驻留在/proc目录下,该目录下有很多文件以暴露内核信息,方便进程来从中读取信息,并在某种情形下还可以使用系统的I/O调用来修改这些信息。说/proc文件系统是虚的,主要是因为它下面的文件及子目录并不占用硬盘空间。相反是内核在进程访问这些信息的时候创建的。

获取某个进程的一些信息

对于每一个系统中进程来说,内核提供了一个在/proc中相应的目录,该目录一般是进程的PID。在/proc/PID目录中有一些文件及子目录等,这些文件及目录都包含了该进程的相关信息。在Gun/Linux系统中,我们最熟悉的1号进程就是init进程,与其相关的进程信息在/proc/1目录下。

在/proc/1目录下,有一个名为status的文件,status文件包含了关于该进程的一些详尽信息,接下来我们可以看看里面是什么东东,

<code>[root@lavenliu ~]</code><code># cat /proc/1/status </code>

<code>Name:   init</code>

<code>State:  S (sleeping)</code>

<code>Tgid:   1</code>

<code>Pid:    1</code>

<code>PPid:   0</code>

<code>TracerPid:  0</code>

<code>Uid:    0   0   0   0</code>

<code>Gid:    0   0   0   0</code>

<code>Utrace: 0</code>

<code>FDSize: 64</code>

<code>Groups:</code>

<code>VmPeak:    19296 kB</code>

<code>VmSize:    19232 kB</code>

<code>VmLck:         0 kB</code>

<code>VmHWM:      1588 kB</code>

<code>VmRSS:      1588 kB</code>

<code>VmData:      200 kB</code>

<code>VmStk:        88 kB</code>

<code>VmExe:       140 kB</code>

<code>VmLib:      2348 kB</code>

<code>VmPTE:        56 kB</code>

<code>VmSwap:        0 kB</code>

<code>Threads:    1</code>

<code>SigQ:   0</code><code>/3878</code>

<code>SigPnd: 0000000000000000</code>

<code>ShdPnd: 0000000000000000</code>

<code>SigBlk: 0000000000000000</code>

<code>SigIgn: 0000000000001000</code>

<code>SigCgt: 00000001a0016623</code>

<code>CapInh: 0000000000000000</code>

<code>CapPrm: ffffffffffffffff</code>

<code>CapEff: fffffffffffffeff</code>

<code>CapBnd: ffffffffffffffff</code>

<code>Cpus_allowed:   3</code>

<code>Cpus_allowed_list:  0-1</code>

<code>Mems_allowed:   00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001</code>

<code>Mems_allowed_list:  0</code>

<code>voluntary_ctxt_switches:    577</code>

<code>nonvoluntary_ctxt_switches: 69</code>

下面列出/proc/PID目录下的一些其他文件,如下:

文件

说明

cmdline

以'\0'结尾的命令行参数

cwd

指向当前工作目录的软连接

environ

环境变量列表,形如:NAME=value形式

exe

执行进程可执行文件的软连接

fd

该目录包含进程已打开的文件描述符,软链接至打开的文件

maps

内存映射

mem

进程的虚拟内存

mounts

进程使用的挂载点

root

软链接至根目录

status

进程的一些信息(如:进程ID、内存使用情况、信号等)

task

该目录下是进程的线程目录,每个线程一个子目录

/proc/PID/fd目录介绍

/proc/PID/fd目录中包含了该PID进程打开的每一个文件描述符的软链接。每一个软链接的名字都与该进程打开的文件描述符是一致的。举例来说,/proc/1990/1这个软链接指向该1990进程的标准输出。

任何一个进程都很方便地访问它自己的/proc/PID目录通过使用/proc/self软链接。

线程:/proc/PID/task目录

Linux内核2.4版本增加了线程组的概念以符合POSIX标准的线程模型。由于线程组里的线程的某些线程属性不同,所以Linux2.4内核版本在/proc/PID目录中增加了task子目录。对于进程的每个线程都对应一个目录,每个目录都是以线程ID命名的,形如/proc/PID/task/TID形式,TID为进程的一个线程。比如我们看一下PID为1287的进程的线程,它有如下的线程在运行,这些目录名称就是1287进程的线程ID(可以使用gettid()来获取线程的TID)

<code>ls</code> <code>/proc/1287/task</code>

<code>1287  1297  1343  1528  1530  1680  1695  1776  1778  1863</code>

<code>1296  1342  1527  1529  1679  1694  1775  1777  1862</code>

在每一个/proc/PID/task/TID子目录下也是一系列的文件和目录,与/proc/PID目录下的文件看起来很像。由于线程间共享了很多属性,这些目录下的文件所包含的信息是一样的。既然这些线程间都共享大量的相同属性,究竟是什么然它们得以区别于其他线程呢?在/proc/PID/task/TID/status文件中包含的信息可以区分于该线程组中的其他线程,这些信息有State,Pid,SigPnd,SigBlk,CapInh,CapPrm,CapEff和CapBnd等信息来区分于其他线程。如,

<code>[root@mydevops task]</code><code># cat 1287/status </code>

<code>Name:   salt-master</code>

<code>Tgid:   1287</code>

<code>Pid:    1287</code>

<code>PPid:   1282</code>

<code>FDSize: 128</code>

<code>VmPeak:  1140892 kB</code>

<code>VmSize:  1075356 kB</code>

<code>VmHWM:     26800 kB</code>

<code>VmRSS:     26800 kB</code>

<code>VmData:   797808 kB</code>

<code>VmStk:       260 kB</code>

<code>VmExe:         4 kB</code>

<code>VmLib:     11504 kB</code>

<code>VmPTE:       520 kB</code>

<code>Threads:    19</code>

<code>SigIgn: 0000000001001000</code>

<code>SigCgt: 0000000180000a02</code>

<code>CapEff: ffffffffffffffff</code>

<code>voluntary_ctxt_switches:    437</code>

<code>nonvoluntary_ctxt_switches: 256</code>

<code>[root@mydevops task]</code><code># cat 1297/status </code>

<code>Pid:    1297</code>

<code>SigBlk: fffffffe7ffbfeff</code>

<code>voluntary_ctxt_switches:    49</code>

<code>nonvoluntary_ctxt_switches: 7</code>

/proc目录下的系统信息

在/proc目录下有一些文件及目录用来提供操作系统级别的信息。这些文件及目录如下表所示:

目录

提供的信息

/proc

一些系统信息

/proc/net

网络及套接字的状态信息

/proc/sys/fs

与文件系统相关的设置

/proc/sys/kernel

与内核相关的一些设置

/proc/sys/net

网络与套接字相关的设置

/proc/sys/vm

内存管理相关的设置

/proc/sysvipc

SystemV IPC相关的信息

访问/proc下面的文件

对于/proc目录下面的很多文件,我们可以通过shell脚本来轻松地访问(也可以使用Python与Perl等脚本语言来访问之),如

<code>[root@mydevops ~]</code><code># cat /proc/sys/kernel/pid_max </code>

<code>32768</code>

<code>[root@mydevops ~]</code><code># echo 100000 &gt; /proc/sys/kernel/pid_max </code>

<code>100000</code>

<code>[root@mydevops ~]</code><code>#</code>

/proc下的文件同样可以被程序通过使用一般的I/O系统调用来访问。程序使用I/O系统调用的方式访问时有如下的限制:

/proc下的一些文件是只读的;那就是说它们的存在只是显示内核的信息,并不能用来修改内核的信息。这些限制同样适用于/proc/PID目录下的文件。

/proc下的一些文件对文件的所有者是可以只读的(或者是一个已授权的进程)。例如,/proc/PID目录下的所有文件通常是拥有该进程的用户所拥有,并且这些文件(如/proc/PID/environ)通常是授权只读权限给文件的所有者。

除了/proc/PID子目录下的文件,/proc下的大部分文件的拥有者是root用户,通常这些文件有的是只可以通过root用户来修改的。

访问/proc/PID目录下的文件

/proc/PID目录下的文件是经常变动的。当一个进程启动时,相应的在/proc目录下会创建以该进程ID的目录;又当该进程终止时,该/proc/PID目录就会消失。

接下来,我们写一个简单的程序,来更新/proc/sys/kernel/pid_max文件,该程序需要从命令行中读取一个参数,并更新原有的数值;如果不提供参数,则获取原先的数值。程序如下:

<code># cat mypid_max.c</code>

<code>#include &lt;stdio.h&gt;</code>

<code>#include &lt;stdlib.h&gt;</code>

<code>#include &lt;string.h&gt;</code>

<code>#include &lt;unistd.h&gt;</code>

<code>#include &lt;sys/types.h&gt;</code>

<code>#include &lt;fcntl.h&gt;</code>

<code>#define MAX_LINE 100</code>

<code>int main(int argc, char *argv[])</code>

<code>{</code>

<code>    </code><code>int     fd;</code>

<code>    </code><code>char    line[MAX_LINE];</code>

<code>    </code><code>ssize_t n;</code>

<code>    </code><code>fd = </code><code>open</code><code>(</code><code>"/proc/sys/kernel/pid_max"</code><code>, (argc &gt; 1) ? O_RDWR : O_RDONLY);</code>

<code>    </code><code>if</code> <code>(fd == -1) {</code>

<code>        </code><code>perror(</code><code>"open"</code><code>);</code>

<code>        </code><code>exit</code><code>(1);</code>

<code>    </code><code>}</code>

<code>    </code> 

<code>    </code><code>n = </code><code>read</code><code>(fd, line, MAX_LINE);</code>

<code>    </code><code>if</code> <code>(n == -1) {</code>

<code>        </code><code>perror(</code><code>"read"</code><code>);</code>

<code>    </code><code>if</code> <code>(argc &gt; 1) {</code>

<code>        </code><code>printf</code><code>(</code><code>"Old value: "</code><code>);</code>

<code>    </code><code>printf</code><code>(</code><code>"%.*s"</code><code>, (int) n, line);</code>

<code>        </code><code>if</code> <code>(lseek(fd, 0, SEEK_SET) == -1) {</code>

<code>            </code><code>perror(</code><code>"lseek"</code><code>);</code>

<code>            </code><code>exit</code><code>(1);</code>

<code>        </code><code>}</code>

<code>        </code><code>if</code> <code>(write(fd, argv[1], strlen(argv[1])) != strlen(argv[1])) {</code>

<code>            </code><code>perror(</code><code>"write() failed"</code><code>);</code>

<code>        </code><code>system(</code><code>"echo /proc/sys/kernel/pid_max now contains "</code>

<code>               </code><code>"`cat /proc/sys/kernel/pid_max`"</code><code>);</code>

<code>    </code><code>exit</code><code>(EXIT_SUCCESS);</code>

<code>}</code>

编译并运行:

<code>gcc -o mypid_max mypid_max.c</code>

<code>[root@mydevops sysinfo]</code><code># ./mypid_max </code>

<code>[root@mydevops sysinfo]</code><code># ./mypid_max 1024</code>

<code>Old value: 100000</code>

<code>/proc/sys/kernel/pid_max</code> <code>now contains 1024</code>

<code>1024</code>

系统鉴别:uname()

uname()系统调用通常会返回一些系统相关的信息。使用的结构体为utsbuf。函数原型为:

<code>#include &lt;sys/utsname.h&gt;</code>

<code>int</code> <code>uname(</code><code>struct</code> <code>utsname *utsbuf);</code>

<code>                                        </code><code>Returns 0 on success, or –1 on error</code>

utsbuf参数是一个指向utsname结构体的指针,它的定义如下:

<code>#define _UTSNAME_LENGTH 65</code>

<code>struct</code> <code>utsname {</code>

<code>    </code><code>char</code> <code>sysname[_UTSNAME_LENGTH]; </code><code>/* Implementation name */</code>

<code>    </code><code>char</code> <code>nodename[_UTSNAME_LENGTH]; </code><code>/* Node name on network */</code>

<code>    </code><code>char</code> <code>release[_UTSNAME_LENGTH]; </code><code>/* Implementation release level */</code>

<code>    </code><code>char</code> <code>version[_UTSNAME_LENGTH]; </code><code>/* Release version level */</code>

<code>    </code><code>char</code> <code>machine[_UTSNAME_LENGTH]; </code><code>/* Hardware on which system</code>

<code>                      </code><code>is running */</code>

<code>#ifdef _GNU_SOURCE /* Following is Linux-specific */</code>

<code>    </code><code>char</code> <code>domainname[_UTSNAME_LENGTH]; </code><code>/* NIS domain name of host */</code>

<code>#endif</code>

<code>};</code>

在CentOS6.5 64位系统上,可以在/proc/sys/kernel目录中找到这些相关的信息。可以通过访问/proc/sys/kernel/hostname,/proc/sys/kernel/osrelease,/proc/sys/kernel/version这三个文件来获取这些信息。这些信息都是只读的,我们可以看看这几个文件的内容:

<code>[root@mydevops sysinfo]</code><code># cat /proc/sys/kernel/hostname </code>

<code>mydevops</code>

<code>[root@mydevops sysinfo]</code><code># cat /proc/sys/kernel/osrelease </code>

<code>2.6.32-573.18.1.el6.x86_64</code>

<code>[root@mydevops sysinfo]</code><code># cat /proc/sys/kernel/version </code>

<code>#1 SMP Tue Feb 9 22:46:17 UTC 2016</code>

另外一个文件/proc/version包含了上面三个文件的所有信息,并且还有一些关于内核编译相关的信息(诸如:是谁进行的内核编译工作,使用的是什么机器进行编译的,使用的GCC版本是多少等附加信息)。我们可以看看/proc/version文件的内容:

<code>[root@mydevops sysinfo]</code><code># cat /proc/version </code>

<code>Linux version 2.6.32-573.18.1.el6.x86_64 ([email protected]) (gcc version 4.4.7 20120313 (Red Hat 4.4.7-16) (GCC) ) </code><code>#1 SMP Tue Feb 9 22:46:17 UTC 2016</code>

utsname结构体中sysname,release,version与machine字段由内核自动设置。接下来看看如何使用uname()函数的使用,代码如下:

<code>[root@mydevops sysinfo]</code><code># cat my_uname.c </code>

<code>#ifdef __linux__</code>

<code>#define _GNU_SOURCE</code>

<code>    </code><code>struct utsname uts;</code>

<code>    </code><code>if</code> <code>(</code><code>uname</code><code>(&amp;uts) == -1) {</code>

<code>        </code><code>perror(</code><code>"uname"</code><code>);</code>

<code>    </code><code>printf</code><code>(</code><code>"Node name:   %s\n"</code><code>, uts.nodename);</code>

<code>    </code><code>printf</code><code>(</code><code>"System name: %s\n"</code><code>, uts.sysname);</code>

<code>    </code><code>printf</code><code>(</code><code>"Release:     %s\n"</code><code>, uts.release);</code>

<code>    </code><code>printf</code><code>(</code><code>"Version      %s\n"</code><code>, uts.version);</code>

<code>    </code><code>printf</code><code>(</code><code>"Machine:     %s\n"</code><code>, uts.machine);</code>

<code>#ifdef _GNU_SOURCE</code>

<code>    </code><code>printf</code><code>(</code><code>"Domain name: %s\n"</code><code>, uts.domainname);</code>

<code>    </code><code>return</code> <code>0;</code>

编译并运行该程序,如下:

<code>[root@mydevops sysinfo]</code><code># gcc -o my_uname my_uname.c </code>

<code>[root@mydevops sysinfo]</code><code># ./my_uname </code>

<code>Node name:   mydevops</code>

<code>System name: Linux</code>

<code>Release:     2.6.32-573.18.1.el6.x86_64</code>

<code>Version      </code><code>#1 SMP Tue Feb 9 22:46:17 UTC 2016</code>

<code>Machine:     x86_64</code>

<code>Domain name: (none)</code>

nodename字段是使用sethostname()系统调用作为其返回值的。domainname字段是使用setdomainname()系统调用作为其返回值的,它是该主机的NIS(Network Information Services)域名。

我们可以使用hostname及domainname进行设置我们的主机名及NIS域名,不过我们很少在程序中使用sethostname()及setdomainname()系统调用。一般地,hostname及NIS domain name是在系统启动时由启动脚本进行设置的。

想要查看更多的关于proc相关的信息,我们查看man page的第5章节的proc帮助信息,如下即可:

<code>man</code> <code>5 proc</code>

练习:

获取系统用户的进程PID及进程名称。该程序需要一个命令行参数,该参数是/etc/passwd中的用户名(如root,mysql等)。

<code>cat</code> <code>my_procfs_user_exe.c</code>

<code>#include &lt;dirent.h&gt;</code>

<code>#include &lt;ctype.h&gt;</code>

<code>#include &lt;limits.h&gt;</code>

<code>#include &lt;pwd.h&gt;</code>

<code>#include &lt;errno.h&gt;</code>

<code>#define MAX_LINE 1000</code>

<code>typedef enum {FALSE, TRUE} Boolean;</code>

<code>char *username_from_id(uid_t uid)</code>

<code>    </code><code>struct </code><code>passwd</code> <code>*</code><code>pwd</code><code>;</code>

<code>    </code><code>pwd</code> <code>= getpwuid(uid);</code>

<code>    </code><code>return</code> <code>(</code><code>pwd</code> <code>== NULL) ? NULL : </code><code>pwd</code><code>-&gt;pw_name;</code>

<code>uid_t userid_from_name(const char *name)</code>

<code>    </code><code>struct </code><code>passwd</code>  <code>*</code><code>pwd</code><code>;</code>

<code>    </code><code>uid_t           u;</code>

<code>    </code><code>char           *endptr;</code>

<code>    </code><code>if</code> <code>(name == NULL || *name == </code><code>'\0'</code><code>) {</code>

<code>        </code><code>return</code> <code>-1;</code>

<code>    </code><code>u = strtol(name, &amp;endptr, 10);</code>

<code>    </code><code>if</code> <code>(*endptr == </code><code>'\0'</code><code>) {</code>

<code>        </code><code>return</code> <code>u;</code>

<code>    </code><code>pwd</code> <code>= getpwnam(name);</code>

<code>    </code><code>if</code> <code>(</code><code>pwd</code> <code>== NULL) {</code>

<code>    </code><code>return</code> <code>pwd</code><code>-&gt;pw_uid;</code>

<code>    </code><code>DIR            *dirp;</code>

<code>    </code><code>struct dirent  *dp;</code>

<code>    </code><code>char            path[PATH_MAX];</code>

<code>    </code><code>char            line[MAX_LINE], cmd[MAX_LINE];</code>

<code>    </code><code>FILE           *fp;</code>

<code>    </code><code>char           *p;</code>

<code>    </code><code>uid_t           uid, checked_uid;</code>

<code>    </code><code>Boolean         got_name, got_uid;</code>

<code>    </code><code>if</code> <code>(argc &lt; 2 || strcmp(argv[1], </code><code>"--help"</code><code>) == 0) {</code>

<code>        </code><code>printf</code><code>(</code><code>"%s &lt;username&gt;\n"</code><code>, argv[0]);</code>

<code>    </code><code>checked_uid = userid_from_name(argv[1]);</code>

<code>    </code><code>if</code> <code>(checked_uid == -1) {</code>

<code>        </code><code>printf</code><code>(</code><code>"Bad username: %s\n"</code><code>, argv[1]);</code>

<code>    </code><code>dirp = opendir(</code><code>"/proc"</code><code>);</code>

<code>    </code><code>if</code> <code>(dirp == NULL) {</code>

<code>        </code><code>perror(</code><code>"opendir"</code><code>);</code>

<code>    </code><code>/* scan entries unser </code><code>/proc</code> <code>directory */</code>

<code>    </code><code>for</code> <code>( ;; ) {</code>

<code>        </code><code>errno = 0;</code>

<code>        </code><code>dp = readdir(dirp);</code>

<code>        </code><code>if</code> <code>(dp == NULL) {</code>

<code>            </code><code>if</code> <code>(errno != 0) {</code>

<code>                </code><code>perror(</code><code>"readdir"</code><code>);</code>

<code>                </code><code>exit</code><code>(1);</code>

<code>            </code><code>} </code><code>else</code> <code>{</code>

<code>                </code><code>break</code><code>;</code>

<code>            </code><code>}</code>

<code>        </code><code>/* since we are looking </code><code>for</code> <code>/proc/PID</code> <code>directories, skip</code>

<code>           </code><code>entries that are not directories, or don't begin with a</code>

<code>           </code><code>digit*/</code>

<code>        </code><code>if</code> <code>(dp-&gt;d_type != DT_DIR || !isdigit((unsigned char) dp-&gt;d_name[0])) {</code>

<code>            </code><code>continue</code><code>;</code>

<code>        </code><code>snprintf(path, PATH_MAX, </code><code>"/proc/%s/status"</code><code>, dp-&gt;d_name);</code>

<code>        </code><code>fp = fopen(path, </code><code>"r"</code><code>);</code>

<code>        </code><code>if</code> <code>(fp == NULL) {</code>

<code>        </code><code>got_name = FALSE;</code>

<code>        </code><code>got_uid = FALSE;</code>

<code>        </code><code>while</code> <code>(!got_name || !got_uid) {</code>

<code>            </code><code>if</code> <code>(fgets(line, MAX_LINE, fp) == NULL) {</code>

<code>            </code><code>/* The </code><code>"Name:"</code> <code>line contains the name of the </code><code>command</code> <code>that</code>

<code>               </code><code>this process is running */</code>

<code>            </code><code>if</code> <code>(strncmp(line, </code><code>"Name:"</code><code>, 5) == 0) {</code>

<code>                </code><code>for</code> <code>(p = line + 5; *p != </code><code>'\0'</code> <code>&amp;&amp; isspace((unsigned char) *p); ) {</code>

<code>                    </code><code>p++;</code>

<code>                </code><code>}</code>

<code>                </code><code>strncpy(cmd, p, MAX_LINE - 1);</code>

<code>                </code><code>cmd[MAX_LINE -1] = </code><code>'\0'</code><code>;        /* Ensure null-terminated */</code>

<code>                </code><code>got_name = TRUE;</code>

<code>            </code><code>/* The </code><code>"Uid:"</code> <code>line contains the real, effective, saved </code><code>set</code><code>-,</code>

<code>               </code><code>and </code><code>file</code><code>-system user IDs */</code>

<code>            </code><code>if</code> <code>(strncmp(line, </code><code>"Uid:"</code><code>, 4) == 0) {</code>

<code>                </code><code>uid = strtol(line + 4, NULL, 10);</code>

<code>                </code><code>got_uid = TRUE;</code>

<code>        </code><code>fclose(fp);</code>

<code>        </code><code>/* If we found a username and a UID, and the UID matches,</code>

<code>           </code><code>then</code> <code>display the PID and </code><code>command</code> <code>name */</code>

<code>        </code><code>if</code> <code>(got_name &amp;&amp; got_uid &amp;&amp; uid == checked_uid)</code>

<code>            </code><code>printf</code><code>(</code><code>"%5s %s"</code><code>, dp-&gt;d_name, cmd);</code>

编译并运行,

<code>[root@mydevops sysinfo]</code><code># ./my_procfs_user_exe mysql</code>

<code> </code><code>1208 mysqld</code>

<code>[root@mydevops sysinfo]</code><code># ./my_procfs_user_exe nginx</code>

<code>[root@mydevops sysinfo]</code><code># ./my_procfs_user_exe daemon</code>

<code>[root@mydevops sysinfo]</code><code># ./my_procfs_user_exe apache</code>

<code> </code><code>3907 httpd</code>

<code> </code><code>3908 httpd</code>

<code> </code><code>3909 httpd</code>

<code> </code><code>3910 httpd</code>

<code> </code><code>3911 httpd</code>

<code> </code><code>3912 httpd</code>

<code> </code><code>3913 httpd</code>

<code> </code><code>3914 httpd</code>

<code> </code><code>3915 httpd</code>

版权声明:原创作品,如需转载,请注明出处。否则将追究法律责任

本文转自    bigstone2012   51CTO博客,原文链接:http://blog.51cto.com/lavenliu/1783110

继续阅读