概述

在实际工作中会经常遇到一些bug,有些就需要用到文件句柄,文件描述符等概念,比如报错: too many open files, 如果你对相关知识一无所知,那么debug起来将会异常痛苦。在linux操作系统中,文件句柄(包括Socket句柄)、打开文件、文件指针、文件描述符的概念比较绕,而且windows的文件句柄又与此有何关联和区别?这一系列的问题是我们不得不面对的。

笔者通过翻阅相关资料,并采用了一些demo来验证相关观点。如果文中有理解偏差,欢迎指正。

这里先笼统的将一下笔者对上面的问题的一些理解:

句柄,熟悉Windows编程的人知道,句柄是Windows用来标识被应用程序所建立或使用的对象的唯一整数,windows使用各种各样的句柄标识诸如应用程序实例、窗口、控制、位图等。Windows的句柄有点像C语言中的文件句柄。更通俗的理解,句柄是一种指向指针的指针。

在linux系统中文件句柄(file handles)和文件描述符(file descriptor)是一个一一对应的关系(如果错误,欢迎指正),按照C语言的理解文件句柄是FILE*(fopen()返回),而文件描述符是fd(int型,open()函数返回),FILE这个结构体中有一个字段是_fileno,其就是指fd(文章末尾通过程序验证),且FILE*和fd可以通过C语言函数进行互相转换,故此笔者认为linux的文件句柄和文件描述符应该是一个一一对应的关系。文件指针即指FILE*,即指文件句柄。打开文件(open files)包括文件句柄但不仅限于文件句柄,由于linux所有的事物都以文件的形式存在,要使用诸如共享内存、信号量、消息队列、内存映射等都会打开文件,但这些是不会占用文件句柄。

ulimit

查看进程允许打开的最大文件句柄数:ulimit -n。设置进程能打开的最大文件句柄数:ulimit -n xxx。

ulimit在系统允许的情况下,提供对特定shell可利用的资源的控制(Provides control over the resources avaliable to the shell and to processes started by it, on systems that allow such control)。-H和-S选项设定指定资源的硬限制和软限制。硬限制设定之后不能再添加,而软限制则可以增加到硬限制规定的值。如果-H和-S选项都没有指定,则软限制和硬限制同时设定。限制值可以是指定资源的数值或者hard, soft, unlimited这些特殊值,其中hard代表当前硬限制, soft代表当前软件限制, unlimited代表不限制. 如果不指定限制值, 则打印指定资源的软限制值, 除非指定了-H选项.如果指定了不只一种资源, 则限制名和单位都会在限制值前显示出来.

[root@hidden ~]# ulimit -Sn
1024
[root@hidden ~]# ulimit -Hn
4096

需要注意的是ulimit提供的是对特定shell可利用的资源的控制,而shell是与具体用户相关的。因此ulimit提供的是对单个用户的限制。包括以下项:

[root@hidden ~]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 62799
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 10240
cpu time               (seconds, -t) unlimited
max user processes              (-u) 65536
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

其中就有个“open files”的限制,默认是1024,也就是这个用户最大可以打开1024个文件。如果使用ulimit -n修改最大文件打开数,那么只对当前shell用户有用,同时也只对当前shell和这个shell fork出来的子shell生效,重启之后会重新恢复为默认值。

limits.conf

limits.conf这个文件是在/etc/security/目录下,因此这个文件是出于安全考虑的。limits.conf文件是用于提供对系统中的用户所使用的资源进行控制和限制,对所有用户的资源设定限制是非常重要的,这可以防止用户发起针对处理器和内存数量等的拒绝服务攻击。这些限制必须在用户登录时限制。

[root@hidden ~]#  cat /etc/security/limits.conf
(省略若干....)
# End of file
apps soft nofile 65535
apps hard nofile 65535
apps soft nproc 10240
apps hard nproc 10240

其中含义如下:

  • 第一列表示域(domain),可以使用用户名(root等),组名(以@开头),通配置*和%,%可以用于%group参数。

  • 第二列表示类型(type),值可以是soft或者hard

  • 第三列表示项目(item),值可以是core, data, fsize, memlock, nofile, rss, stack, cpu, nproc, as, maxlogins, maxsyslogins, priority, locks, msgqueue, nie, rtprio。其中nofile(Number of Open File)就是文件打开数。

  • 第四列表示值.

关于第三列的详细解释如下:

#<item> can be one of the following:
#        - core - limits the core file size (KB)
#        - data - max data size (KB)
#        - fsize - maximum filesize (KB)
#        - memlock - max locked-in-memory address space (KB)
#        - nofile - max number of open file descriptors
#        - rss - max resident set size (KB)
#        - stack - max stack size (KB)
#        - cpu - max CPU time (MIN)
#        - nproc - max number of processes
#        - as - address space limit (KB)
#        - maxlogins - max number of logins for this user
#        - maxsyslogins - max number of logins on the system
#        - priority - the priority to run user process with
#        - locks - max number of file locks the user can hold
#        - sigpending - max number of pending signals
#        - msgqueue - max memory used by POSIX message queues (bytes)
#        - nice - max nice priority allowed to raise to values: [-20, 19]
#        - rtprio - max realtime priority

limits.conf与ulimit的区别在于前者是针对所有用户的,而且在任何shell都是生效的,即与shell无关,而后者只是针对特定用户的当前shell的设定。在修改最大文件打开数时,最好使用limits.conf文件来修改,通过这个文件,可以定义用户,资源类型,软硬限制等。也可修改/etc/profile文件加上ulimit的设置语句来是的全局生效。
当达到上限时,会报错:too many open files或者遇上Socket/File: Cannot open so many files等。

file-max & file-nr

[root@hidden ~]# cat /proc/sys/fs/file-max
798282
[root@hidden fd]# sysctl -a | grep fs.file-max
fs.file-max = 798282

该文件指定了可以分配的文件句柄的最大数目(系统全局的可用句柄数目. The value in file-max denotes the maximum number of file handles that the Linux kernel will allocate)。如果用户得到的错误消息诸如“由于打开文件数已经达到了最大值”之类,那么说明他们不能打开更多文件,则可能需要增加该值。可将这个值设置成任意多个文件,并且能通过将一个新数字值写入该文件来更改该值。这个参数的默认值和内存大小有关系,可以使用公式:file-max ≈ 内存大小/ 10k.

[root@hidden ~]# cat /proc/sys/fs/file-nr
1440        0   798282

关于file-nr参数的解释如下:
Historically, the three values in file-nr denoted the number of allocated file handles, the number of allocated but unused file handles, and the maximum number of file handles. Linux 2.6 always reports 0 as the number of free file handles – this is not an error, it just means that the number of allocated file handles exactly matches the number of used file handles.

这三个值分别指:系统已经分配出去的句柄数、已经分配但是还没有使用的句柄数以及系统最大的句柄数(和file-max一样)。

[root@hidden fd]# lsof | wc -l
2538

lsof是列出系统所占用的资源(list open files),但是这些资源不一定会占用句柄。比如共享内存、信号量、消息队列、内存映射等,虽然占用了这些资源,但不占用句柄。
如果出了某些故障,使用lsof | wc -l的结果,这个时候可以通过file-nr粗略的估算一下。

查看硬盘信息:df -m
查看内存信息:free -m
查看CPU信息:cat /proc/cpuinfo
查看内核所能打开的线程数:cat /proc/sys/kernel/threads-max

为什么有限制?

为什么Linux内核对文件句柄数、线程和进程的最大打开数进行了限制?以及如果我们把它调的太大,会产生什么样的后果?

原因1 - 资源问题:the operating system needs memory to manage each open file, and memory is a limited resource - especially on embedded systems.
原因2 - 安全问题:if there were no limits, a userland software would be able to create files endlessly until the server goes down.

What’s more? If the file descriptors are tcp sockets, etc, then you risk using up a large amount for the socket buffers and other kernel objects, this memory is not going to be swappable.

最主要的是资源问题,为防止某一单一进程打开过多文件描述符而耗尽系统资源,对进程打开文件数做了限制。

lsof

lsof(list open files)是一个列出当前系统打开文件的工具。在linux环境下,任何事物都以文件的形式存在,通过文件不仅仅可以访问常规数据,还可以访问网络连接和硬件。所以如TCP和UDP等,系统在后台都为该应用程序分配了一个文件描述符,无论这个文件的本质如何,该文件描述符为应用程序与基础操作系统之间的交互提供了通用接口。因为应用程序打开文件的描述符列表提供了大量关于这个应用程序本身的信息,因此通过lsof工具能够查看这个列表对系统检测以及拍错将是很有帮助的。

在终端下输入lsof即可显示系统打开的文件,因为lsof需要访问核心内存和各种文件,所以必须以root身份运行它才能够充分地发挥其功能。

[root@hidden linuxC]# lsof -p 14895
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF    NODE NAME
java    14895 root  cwd    DIR              252,1     4096 1310824 /root/util/kafka_2.10-0.8.2.1
java    14895 root  rtd    DIR              252,1     4096       2 /
java    14895 root  txt    REG              252,1     7734 1583642 /root/util/jdk1.8.0_112/bin/java
java    14895 root  mem    REG              252,1 10485760 1325066 /tmp/kafka-logs/default_channel_kafka_zzh_demo-4/00000000000003626728.index
...(省略若干)
java    14895 root   85u   REG              252,1        0 1311594 /tmp/kafka-logs/default_channel_kafka_zzh_demo-4/00000000000003626728.log
java    14895 root   87u   REG              252,1        0 1325038 /tmp/kafka-logs/default_channel_kafka_zzh_demo-3/00000000000003915669.log
java    14895 root   88u  IPv6           40855648      0t0     TCP zhuzhonghua2-fqawb:XmlIpcRegSvc->xx.xx.139.85:64708 (ESTABLISHED)
java    14895 root   89u   REG              252,1        0 1325037 /tmp/kafka-logs/default_channel_kafka_zzh_demo-2/00000000000005892533.log
java    14895 root   93u   REG              252,1        0 1325040 /tmp/kafka-logs/default_channel_kafka_zzh_demo-1/00000000000005494790.log
java    14895 root   94u   REG              252,1        0 1325043 /tmp/kafka-logs/default_channel_kafka_zzh_demo-0/00000000000003858999.log
[root@hidden linuxC]# ls /proc/14895/fd | wc -l
89
[root@hidden linuxC]# ls /proc/14895/fd
0  10  12  14  16  18  2   21  23  25  27  29  30  32  34  36  38  4   41  43  45  47  49  50  52  54  56  58  6   61  63  65  67  69  70  72  75  77  79  80  82  85  88  9   94
1  11  13  15  17  19  20  22  24  26  28  3   31  33  35  37  39  40  42  44  46  48  5   51  53  55  57  59  60  62  64  66  68  7   71  74  76  78  8   81  83  87  89  93

lsof输出各列信息的意义如下:

COMMAND:进程的名称
PID: 进程标识符
USER:进程所有者
FD:文件描述符,应用程序通过文件描述符识别该文件。如cwd, rtd, txt, mem, DEL, 0u, 3w, 4r等
TYPE:文件类型,如DIR, REG, CHR, Ipv6, unix, FIFO等
DEVICE:指定磁盘的名称
SIZE/OFF:文件的大小
NODE:索引节点
NAME:打开文件的确切名称

FD列中的文件描述符cwd表示应用程序的当前工作目录,这是该应用程序启动的目录,除非它本身对这个目录进行更改;txt类型的文件是程序代码,如应用程序二进制文件本身或共享库,如上列表中显示的、sbin/init程序;数值表示应用程序的文件描述符,这是打开该文件时返回的一个整数,如“lsof -p 14895”命令解析出来的最后一行的文件描述符为94,u表示该文件被打开处于读写模式,而不是只读r或只写w模式,同时还有大写的W表示该应用程序具有对整个文件的写锁。该文件描述符用于确保每次只能打开一个应用程序实例。初始打开每个应用程序时,都有三个文件描述符:0、1、2,分别表示标准输入、标准输出、错误流。所以大多数应用程序所打开的文件的FD都是从3开始的。

TYPE列比较直观。文件和目录分别为REG和DIR。而CHR和BLK分别表示字符和块设备。或者unix, FIFO, Ipv6分表表示UNIX域套接字,FIFO队列和IP套接字。

查看当前进程打开了多少文件:lsof -n|awk ‘{print $2}’|sort|uniq -c|sort -nr|more | grep [PID]

[root@hidden fd]# lsof -n|awk '{print $2}'|sort|uniq -c|sort -nr|more | grep 14895173 14895

第一列是句柄数,第二列是进程号PID.

[root@hidden proc]# lsof -p 14895 | wc -l
174

这里多了一个是由于:

COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF    NODE NAME
java    14895 root  cwd    DIR              252,1     4096 1310824 /root/util/kafka_2.10-0.8.2.1
java    14895 root  rtd    DIR              252,1     4096       2 /
java    14895 root  txt    REG              252,1     7734 1583642 /root/util/jdk1.8.0_112/bin/java
java    14895 root  mem    REG              252,1 10485760 1325066 /tmp/kafka-logs/default_channel_kafka_zzh_demo-4/00000000000003626728.index
java    14895 root  mem    REG              252,1 10485760 1325044 /tmp/kafka-logs/default_channel_kafka_zzh_demo-0/00000000000003858999.index
java    14895 root  mem    REG              252,1 10485760 1325042 /tmp/kafka-logs/default_channel_kafka_zzh_demo-2/00000000000005892533.index
java    14895 root  mem    REG              252,1 10485760 1325041 /tmp/kafka-logs/default_channel_kafka_zzh_demo-1/00000000000005494790.index
....(省略若干)
java    14895 root   85u   REG              252,1        0 1311594 /tmp/kafka-logs/default_channel_kafka_zzh_demo-4/00000000000003626728.log
java    14895 root   87u   REG              252,1        0 1325038 /tmp/kafka-logs/default_channel_kafka_zzh_demo-3/00000000000003915669.log
java    14895 root   88u  IPv6           40855648      0t0     TCP zhuzhonghua2-fqawb:XmlIpcRegSvc->xx.xx.139.85:64708 (ESTABLISHED)
java    14895 root   89u   REG              252,1        0 1325037 /tmp/kafka-logs/default_channel_kafka_zzh_demo-2/00000000000005892533.log
java    14895 root   93u   REG              252,1        0 1325040 /tmp/kafka-logs/default_channel_kafka_zzh_demo-1/00000000000005494790.log
java    14895 root   94u   REG              252,1        0 1325043 /tmp/kafka-logs/default_channel_kafka_zzh_demo-0/00000000000003858999.log

多了“COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME”这一行。
而文件描述符的个数为90:

[root@zhuzhonghua2-fqawb linuxC]# ls /proc/14895/fd |wc -l
90
[root@zhuzhonghua2-fqawb linuxC]# ls /proc/14895/fd
0  10  12  14  16  18  2   21  23  25  27  29  30  32  34  36  38  4   41  43  45  47  49  50  52  54  56  58  6   61  63  65  67  69  70  72  75  77  79  80  82  84  87  89  93
1  11  13  15  17  19  20  22  24  26  28  3   31  33  35  37  39  40  42  44  46  48  5   51  53  55  57  59  60  62  64  66  68  7   71  74  76  78  8   81  83  85  88  9   94

文件描述符(file descriptor)

对于linux而言,所有对设备和文件的操作都使用文件描述符来进行的。文件描述符是一个非负的整数,它是一个索引值,指向内核中每个进程打开文件的记录表。当打开一个现存文件或创建一个新文件时,内核就向进程返回一个文件描述符;当需要读写文件时,也需要把文件描述符作为参数传递给相应的函数。
通常,一个进程启动时,都会打开3个文件:标准输入、标准输出和标准出错处理。这3个文件分别对应文件描述符为0、1和2(宏STD_FILENO、STDOUT_FILENO和STDERR_FILENO)。

每一个文件描述符会与一个打开文件相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。具体情况要具体分析,要理解具体其概况如何,需要查看由内核维护的3个数据结构。

  1. 进程级的文件描述符表

  2. 系统级的打开文件描述符表

  3. 文件系统的i-node表

由于进程级文件描述符表的存在,不同的进程中会出现相同的文件描述符,它们可能指向同一个文件,也可能指向不同的文件。两个不同的文件描述符,若指向同一个打开文件句柄,将共享同一文件偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量,那么从另一个文件描述符中也会观察到变化,无论这两个文件描述符是否属于不同进程,还是同一个进程,情况都是如此。

文件句柄 vs 文件描述符

文件句柄也称为文件指针(FILE *):C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。

C语言中FILE结构体的定义:

/* Define outside of namespace so the C++ is happy.  */
struct _IO_FILE;__BEGIN_NAMESPACE_STD
/* The opaque type of streams.  This is the definition used elsewhere.  */
typedef struct _IO_FILE FILE;
__END_NAMESPACE_STD
#if defined __USE_LARGEFILE64 || defined __USE_SVID || defined __USE_POSIX \|| defined __USE_BSD || defined __USE_ISOC99 || defined __USE_XOPEN \|| defined __USE_POSIX2
__USING_NAMESPACE_STD(FILE)
#endif# define __FILE_defined 1
#endif /* FILE not defined.  */
#undef  __need_FILE#if !defined ____FILE_defined && defined __need___FILE/* The opaque type of streams.  This is the definition used elsewhere.  */
typedef struct _IO_FILE __FILE;
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;signed char _vtable_offset;
char _shortbuf[1];/* char* _save_gptr; char* _save_egptr; */_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

这个_IO_FILE结构体中的“int _fileno”就是fd,即文件描述符。这个可以通过程序验证:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>int main(){char buf[50] = {"file descriptor demo"};FILE *myfile;myfile = fopen("test","w+");if(!myfile){printf("error: openfile failed!\n");}printf("The openfile's descriptor is %d\n", myfile->_fileno);if(write(myfile->_fileno,buf,50)<0){perror("error: write file failed!\n");exit(1);}else{printf("writefile successed!\n");}exit(0);
}

编译:g++ fileno.cpp -o fileno.out
执行+输出:

[root@hidden linuxC]# ./fileno.out
The openfile's descriptor is 3
writefile successed!

查看test文件:

[root@hidden linuxC]# cat test
file descriptor demo

文件句柄?文件描述符?傻傻分不清楚相关推荐

  1. mysql句柄是文件描述符_误删除innodb ibdata数据文件 文件句柄 文件描述符 proc fd...

    误删除innodb ibdata数据文件  文件句柄  文件描述符  proc  fd http://www.cnblogs.com/gomysql/p/3702216.html 提示:如果不小心通过 ...

  2. linux文件描述符 0 1 2,文件描述符

    内核(kernel)利用文件描述符(file descriptor)来访问文件.文件描述符是非负整数.打开现存文件或新建文件时,内核会返回一个文件描述符.读写文件也需要使用文件描述符来指定待读写的文件 ...

  3. 文件句柄(file handles) 文件描述符(file descriptors)

    欢迎支持笔者新作:<深入理解Kafka:核心设计与实践原理>和<RabbitMQ实战指南>,同时欢迎关注笔者的微信公众号:朱小厮的博客. 欢迎跳转到本文的原文链接:https: ...

  4. 关于文件句柄数和文件描述符的区分

    在一次生产环境上,用户那边反馈服务连接redis异常,经过查看redis的日志,报打开文件数过多,通过lsof去查看文件句柄数确实超过了主机限制的句柄数,只能先重启redis进行释放连接数,进行恢复业 ...

  5. linux进程文件描述符 vnode,Linux C编程详解:进程原理分析、文件描述符和文件记录表、文件句柄和文件原理...

    一.引言 文件操作是Linux C编程中其中的一项核心技术,实际上也相当重要,这里并不是说狭义上的那种文件操作,它也非常有助于理解和学习Linux系统.为什么这样说呢?因为在Unix/Linux的世界 ...

  6. 文件描述符与文件句柄

    在我们日常编程中经常会遇到文件描述符(file descriptor)和文件句柄(file handler)这两个概念,特别是需要开发跨平台(跨windows和linux)项目的时候会被这两个概念搞得 ...

  7. linux文件描述符与标识符,文件描述符fd

    这里以问答的方式来讨论这个问题: 1. 文件描述符 fd 和文件指针 FILE *的关系? 文件描述符是什么?我们知道每一个进程都有一个自己的PCB(进程控制块),进程控制块的结构是: struct ...

  8. linux 反弹shell(一)文件描述符与重定向

    0X00 前言 由于在反弹shell的过程中有一些非常精简的语句,但是一直没有深入理解,只是作为一个伸手党/搬运工,于是下定决心要将其弄明白,而这里面最难的也就是文件描述符和重定向的部分,因此我特地写 ...

  9. linux存储--文件描述符以及file结构体(一)

    一.什么是文件描述符 在Linux下一切皆文件,对于内核而言,所有打开的文件都通过文件描述符引用,文件描述符是一个非负整数,当打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符.当读. ...

最新文章

  1. python的主函数如何书写_Python程序的入口在哪里?main函数的恰当写法
  2. Mac OS X 修复基本命令失效的问题
  3. Delphi 7下最小化到系统托盘
  4. Docker网络-(六)
  5. HLS视频点播直播初探
  6. 荣耀6plus+android5.1,荣耀6Plus Emui3.1-Android5.1.1 Root教程
  7. ad19原理图标注_AD19原理图ID复位
  8. Windows设置程序开机自启动的几种方法(整理发布)
  9. html博客音乐播放器代码大全,一款百度FLASH音乐播放器代码
  10. Linux添加用户及用户权限管理
  11. 推荐了这些好书,你看过几本啦?
  12. 机器学习-疑点1 :shape[ ]的理解
  13. 技术团队人员管理:如何快速的组建团队
  14. ant安装配置问题:ANT_HOME is set incorrectly or ant could not be located. Please set ANT_HOME.
  15. 一个优秀IT专家的成长历程---献给所有的颓废或即将颓废的人们(ZZ)
  16. 上海人工智能强在哪?不妨看魔都AI企业50家
  17. 通过wifi热点软件分享网络时,重启的功效
  18. NGUI制作聊天系统
  19. Java核心技术【卷一】——学习笔记(五)--泛型(一)
  20. 高仿QQ2012登录界面

热门文章

  1. Day 10: PhoneGap —— 开发手机应用如此简单
  2. 使用 GitHub, Jekyll 打造自己的免费独立博客
  3. 2015年山石网科面试题
  4. CSS 强制换行和禁止换行强制换行 和禁止换行样式
  5. Oracle VM VirtualBox下各种视图切换
  6. UNIX网络编程——TCP/IP简介
  7. Asigra无代理备份:“云”数据保护的先行者
  8. ipsec ***概念(一)
  9. 双边滤波(bilateral filter)彩色图 matlab实现代码
  10. linux send与recv函数详解