Linux4.4 TCP源码分析
2016-07-09 00:26
357 查看
摘要: 我和我的小伙伴们折腾了一个月的内核TCP源码分析文档终于完工了!欢迎关注~
本博文摘录了其中的部分内容,并给出了原文的Github地址,希望能和大家共同进步~
目前网络上已有大量关于TCP的分析,但是很多博文分析的版本还停留在很早的阶段。而且一般都是TCP中很小的一个部分。我们希望能够更系统地分析目前较新版本的内核TCP实现。
当然了,作为学生作品,难免有各种不足,希望能够和大家一道,共同改进这一不成熟的作品。issues、star、watch、pull-request等我们都欢迎~
这张图对于后面的分析有很强的指导意义。
连接部分分为了主动连接和被动连接。主动连接是指客户端从CLOSED状态主动发出连接请求, 进入SYN-SENT状态,之后收到服务端的SYN+ACK包,进入ESTAB状态(即连接建立状态), 然后回复ACK包,完成三次握手。这一部分的代码我们将在[sec:tcp_connect_client] 中进行详细分析。被动连接是从listen状态开始,监听端口。随后收到SYN包,进入SYN-RCVD状态, 同时发送SYN+ACK包,最后,收到ACK后,进入ESTAB状态,完成被动连接的三次握手过程。 这一部分的详细讨论在[sec:tcp_server_connect]中完成。
连接终止的部分也被分为了两部分进行实现,主动终止和被动终止。主动终止是上图中从ESTAB状态 主动终止连接,发送FIN包的过程。可以看到,主动终止又分为两种情况,一种是FIN发出后,收到了 发来的FIN(即通信双方同时主动关闭连接),此时转入CLOSING状态并发送ACK包。收到ACK后, 进入TIME WAIT状态。另一种是收到了ACK包,转入FINWAIT-2状态,最后收到FIN后发送ACK, 完成四次握手,进入TIME WAIT状态。最后等数据发送完或者超时后,删除TCB,进入CLOSED状态。 被动终止则是接收到FIN包后,发送了ACK包,进入CLOSE WAIT状态。之后,当这一端的数据 也发送完成后,发送FIN包,进入LAST-ACK状态,接收到ACK后,进入CLOSED状态。
这张图可以很方便地读出各个位占多长,它上面的标识是十进制的,很容易读。 这里我们挑出我们比较关心的Options字段来解读。因为很多TCP的扩展都是通过新增 选项来实现的。
选项总是在TCP头部的最后,且长度是 8 位的整数倍。全部选项都被包括在 校验和中。选项可从任何字节边界开始。选项的格式有 2 种情况:
单独的一个字节,代表选项的类型例如:
第一个字节代表选项的类型,紧跟着的一个字节代表选项的长度,后面跟着 选项的数据。例如:
三次握手时会拒绝旧的SYN段,以避免重复建立连接。
通过判断序列号可以有效地拒绝旧的或者重复的段被错误地接受。
通过选择合适的ISN(Initial Sequence Number)可以避免旧的连接和新的连接的段 的序列号空间发生重叠。
TIME-WAIT状态会等待足够长的时间,让旧的滞留在网络上的段因超过其生命周期而消失。
在系统崩溃后,在系统启动时的静默时间可以使旧的段在连接开始前消失。
然而,其中的状态的相关机制却是不可靠的。网络中滞留的段 有可能会使得状态被意外结束。这一现象即是TIME-WAIT Assassination现象。RFC1337中给出了一个实例:
可以看到,TCP A收到了一个遗留的ACK包,之后响应了这个ACK。TCP B收到这个莫名其妙的响应后, 会发出RST报,因为它认为发生了错误。收到RST包后,TCP A的TIME-WAIT状态被终止了。然而, 此时还没有到2MSL的时间。
滞留在网络上的旧的数据段可能被错误地接受。
新的连接可能陷入到不同步的状态中。如接收到一个旧的ACK包等情况。
新的连接可能被滞留在网络中的旧的FIN包关闭。或者是SYN-SENT状态下出现了 意料之外的ACK包等。都可能导致新的连接被终止。
而解决TWA问题的方法较为简单,直接在TIME-WAIT阶段忽略掉所有的RST段即可。 在[subsec:time_wait]中的代码中可以看到,Linux正是采用了这种方法来解决该问题。
![](https://static.oschina.net/uploads/img/201607/02125332_W2J7.png)
上面 rcu_dereference_protected函数使用了RCU锁,RCU锁的基本介绍参见[Appendix:RCU].
源地址路由是一种特殊的路由策略。一般路由都是通过目的地址来进行的。而有时也需要 通过源地址来进行路由,例如在有多个网卡等情况下,可以根据源地址来决定走哪个网卡等等。
对于函数IS_ERR、PTR_ERR的具体介绍,请参见[Appendix:ERR].
总结起来,是在根据用户提供的目的地址, 设置好了传输控制块,为传输做好准备。如果在这一过程中出现错误,则会跳到 错误处理代码。
关于tcp_connect_init更多的内容,请参见[TCPInitialize:tcp_connect_init]。
关于tcp_init_nondata_skb更多的内容,请参见[TCPInitialize:tcp_init_nondata_skb]。
本博文摘录了其中的部分内容,并给出了原文的Github地址,希望能和大家共同进步~
概述
出于对Linux内核的兴趣,也得益于学院老师的支持,我和我的小伙伴们完成了一份内核TCP源码的分析文档。基于CC协议开源,项目托管在了Github上:欢迎关注^_^目前网络上已有大量关于TCP的分析,但是很多博文分析的版本还停留在很早的阶段。而且一般都是TCP中很小的一个部分。我们希望能够更系统地分析目前较新版本的内核TCP实现。
当然了,作为学生作品,难免有各种不足,希望能够和大家一道,共同改进这一不成熟的作品。issues、star、watch、pull-request等我们都欢迎~
正文节选
作品较长,我们节选了一部分,以供预览。由于是节选,因而行文逻辑并不连贯。部分的字符画由于从latex转换到markdown问题,难以对齐,显示不出原来的效果。大家可以看我们Github上生成好的完整PDF版本。RFC
在分析TCP的过程中会遇到很多RFC,在这里,我们将可能会碰到的RFC罗列出来, 并进行一定的讨论,便于后面的分析。RFC793 : Transmission Control Protocol
该RFC正是定义了TCP协议的那份RFC。在该RFC中,可以查到TCP的很多细节,帮助后续的代码分析。TCP状态图
在RFC793中,给出了TCP协议的状态图,图中的TCB代表TCP控制块。原图如下所示:+---------+ ---------\ active OPEN | CLOSED | \ ----------- +---------+<---------\ \ create TCB | ^ \ \ snd SYN passive OPEN | | CLOSE \ \ ------------ | | ---------- \ \ create TCB | | delete TCB \ \ V | \ \ +---------+ CLOSE | \ | LISTEN | ---------- | | +---------+ delete TCB | | rcv SYN | | SEND | | ----------- | | ------- | V +---------+ snd SYN,ACK / \ snd SYN +---------+ | |<----------------- ------------------>| | | SYN | rcv SYN | SYN | | RCVD |<-----------------------------------------------| SENT | | | snd ACK | | | |------------------ -------------------| | +---------+ rcv ACK of SYN \ / rcv SYN,ACK +---------+ | -------------- | | ----------- | x | | snd ACK | V V | CLOSE +---------+ | ------- | ESTAB | | snd FIN +---------+ | CLOSE | | rcv FIN V ------- | | ------- +---------+ snd FIN / \ snd ACK +---------+ | FIN |<----------------- ------------------>| CLOSE | | WAIT-1 |------------------ | WAIT | +---------+ rcv FIN \ +---------+ | rcv ACK of FIN ------- | CLOSE | | -------------- snd ACK | ------- | V x V snd FIN V +---------+ +---------+ +---------+ |FINWAIT-2| | CLOSING | | LAST-ACK| +---------+ +---------+ +---------+ | rcv ACK of FIN | rcv ACK of FIN | | rcv FIN -------------- | Timeout=2MSL -------------- | | ------- x V ------------ x V \ snd ACK +---------+delete TCB +---------+ ------------------------>|TIME WAIT|------------------>| CLOSED | +---------+ +---------+ TCP Connection State Diagram
这张图对于后面的分析有很强的指导意义。
连接部分分为了主动连接和被动连接。主动连接是指客户端从CLOSED状态主动发出连接请求, 进入SYN-SENT状态,之后收到服务端的SYN+ACK包,进入ESTAB状态(即连接建立状态), 然后回复ACK包,完成三次握手。这一部分的代码我们将在[sec:tcp_connect_client] 中进行详细分析。被动连接是从listen状态开始,监听端口。随后收到SYN包,进入SYN-RCVD状态, 同时发送SYN+ACK包,最后,收到ACK后,进入ESTAB状态,完成被动连接的三次握手过程。 这一部分的详细讨论在[sec:tcp_server_connect]中完成。
连接终止的部分也被分为了两部分进行实现,主动终止和被动终止。主动终止是上图中从ESTAB状态 主动终止连接,发送FIN包的过程。可以看到,主动终止又分为两种情况,一种是FIN发出后,收到了 发来的FIN(即通信双方同时主动关闭连接),此时转入CLOSING状态并发送ACK包。收到ACK后, 进入TIME WAIT状态。另一种是收到了ACK包,转入FINWAIT-2状态,最后收到FIN后发送ACK, 完成四次握手,进入TIME WAIT状态。最后等数据发送完或者超时后,删除TCB,进入CLOSED状态。 被动终止则是接收到FIN包后,发送了ACK包,进入CLOSE WAIT状态。之后,当这一端的数据 也发送完成后,发送FIN包,进入LAST-ACK状态,接收到ACK后,进入CLOSED状态。
TCP头部格式
RFC793中,对于TCP头部格式的描述摘录如下:0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Acknowledgment Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | |U|A|P|R|S|F| | | Offset| Reserved |R|C|S|S|Y|I| Window | | | |G|K|H|T|N|N| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | data | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ TCP Header Format Note that one tick mark represents one bit position.
这张图可以很方便地读出各个位占多长,它上面的标识是十进制的,很容易读。 这里我们挑出我们比较关心的Options字段来解读。因为很多TCP的扩展都是通过新增 选项来实现的。
选项总是在TCP头部的最后,且长度是 8 位的整数倍。全部选项都被包括在 校验和中。选项可从任何字节边界开始。选项的格式有 2 种情况:
单独的一个字节,代表选项的类型例如:
End of Option List +--------+ |00000000| +--------+ Kind=0 No-Operation +--------+ |00000001| +--------+ Kind=1
第一个字节代表选项的类型,紧跟着的一个字节代表选项的长度,后面跟着 选项的数据。例如:
Maximum Segment Size +--------+--------+---------+--------+ |00000010|00000100| max seg size | +--------+--------+---------+--------+ Kind=2 Length=4
RFC1337 : TIME-WAIT Assassination Hazards in TCP
在TCP连接中,存在这样一个阶段。该阶段会等待2MSL的 时间,以使得属于当前连接的所有的包都消失掉。这样可以保证再次用相同端口建立连接时, 不会有属于上一个连接的滞留在网络中的包对连接产生干扰。TIME-WAIT Assassination(TWA)现象
TCP的连接是由四元组(源IP地址,源端口,目的IP地址,目的端口)唯一决定的。但是,存在 这样一种情况,当一个TCP连接关闭,随后,客户端又使用相同的IP和端口号向服务端发起连接, 即产生了和之前的连接一模一样的四元组。此时,如果网络中还存在上一个连接遗留下来的包, 就会出现各类的问题。对于这一问题,RFC793中定义了相关的机制进行应对。三次握手时会拒绝旧的SYN段,以避免重复建立连接。
通过判断序列号可以有效地拒绝旧的或者重复的段被错误地接受。
通过选择合适的ISN(Initial Sequence Number)可以避免旧的连接和新的连接的段 的序列号空间发生重叠。
TIME-WAIT状态会等待足够长的时间,让旧的滞留在网络上的段因超过其生命周期而消失。
在系统崩溃后,在系统启动时的静默时间可以使旧的段在连接开始前消失。
然而,其中的状态的相关机制却是不可靠的。网络中滞留的段 有可能会使得状态被意外结束。这一现象即是TIME-WAIT Assassination现象。RFC1337中给出了一个实例:
TCP A TCP B 1. ESTABLISHED ESTABLISHED (Close) 2. FIN-WAIT-1 --> <SEQ=100><ACK=300><CTL=FIN,ACK> --> CLOSE-WAIT 3. FIN-WAIT-2 <-- <SEQ=300><ACK=101><CTL=ACK> <-- CLOSE-WAIT (Close) 4. TIME-WAIT <-- <SEQ=300><ACK=101><CTL=FIN,ACK> <-- LAST-ACK 5. TIME-WAIT --> <SEQ=101><ACK=301><CTL=ACK> --> CLOSED - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5.1. TIME-WAIT <-- <SEQ=255><ACK=33> ... old duplicate 5.2 TIME-WAIT --> <SEQ=101><ACK=301><CTL=ACK> --> ???? 5.3 CLOSED <-- <SEQ=301><CTL=RST> <-- ???? (prematurely)
可以看到,TCP A收到了一个遗留的ACK包,之后响应了这个ACK。TCP B收到这个莫名其妙的响应后, 会发出RST报,因为它认为发生了错误。收到RST包后,TCP A的TIME-WAIT状态被终止了。然而, 此时还没有到2MSL的时间。
TWA的危险性及现有的解决方法
RFC1337中列举了三种TWA现象带来的危险。滞留在网络上的旧的数据段可能被错误地接受。
新的连接可能陷入到不同步的状态中。如接收到一个旧的ACK包等情况。
新的连接可能被滞留在网络中的旧的FIN包关闭。或者是SYN-SENT状态下出现了 意料之外的ACK包等。都可能导致新的连接被终止。
而解决TWA问题的方法较为简单,直接在TIME-WAIT阶段忽略掉所有的RST段即可。 在[subsec:time_wait]中的代码中可以看到,Linux正是采用了这种方法来解决该问题。
第一次握手:构造并发送SYN包
基本调用关系
![](https://static.oschina.net/uploads/img/201607/02125332_W2J7.png)
tcp_v4_connect
的主要作用是进行一系列的判断,初始化传输控制块 并调用相关函数发送SYN包。/* Location: net/ipv4/tcp_ipv4.c Function: 这个函数会初始化一个的连接。 Parameters: sk:传输控制块 uaddr:通用地址结构,包含所属协议字段和相应的地址字段。 addr_len :目的地址长度 */ int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) { /* sockaddr_in结构体用于描述一个Internet (IP) 套接字的地址 */ struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; struct inet_sock *inet = inet_sk(sk); struct tcp_sock *tp = tcp_sk(sk); /* 网络协议主要使用大端存储, be16 means 16 bits stored with big-endian be32,the same. */ __be16 orig_sport, orig_dport; __be32 daddr, nexthop; struct flowi4 *fl4; struct rtable *rt; int err; struct ip_options_rcu *inet_opt; /* 检验目的地址长度是否有效 */ if (addr_len < sizeof(struct sockaddr_in)) return -EINVAL; //Invalid argument 错误码为22 /* 检验协议族是否正确 */ if (usin->sin_family != AF_INET) //IPV4地址域 return -EAFNOSUPPORT; //Address family not supported by protocol /* 将下一跳地址和目的地址暂时设置为用户传入的IP地址 */ nexthop = daddr = usin->sin_addr.s_addr; inet_opt = rcu_dereference_protected(inet->inet_opt, sock_owned_by_user(sk)); /* 如果选择源地址路由,则将下一跳地址设置为IP选项中的faddr-first hop address*/ if (inet_opt && inet_opt->opt.srr) { if (!daddr) return -EINVAL; nexthop = inet_opt->opt.faddr; }
上面 rcu_dereference_protected函数使用了RCU锁,RCU锁的基本介绍参见[Appendix:RCU].
源地址路由是一种特殊的路由策略。一般路由都是通过目的地址来进行的。而有时也需要 通过源地址来进行路由,例如在有多个网卡等情况下,可以根据源地址来决定走哪个网卡等等。
orig_sport = inet->inet_sport; orig_dport = usin->sin_port; fl4 = &inet->cork.fl.u.ip4; //对应于ipv4的流 /* 获取目标的路由缓存项,如果路由查找命中,则生成一个相应的路由缓存项, 这个缓存项不但可以用于当前待发送的SYN段,而且对后续的所有数据包都 可以起到一个加速路由查找的作用。 */ rt = ip_route_connect(fl4, nexthop, inet->inet_saddr, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if, IPPROTO_TCP, orig_sport, orig_dport, sk); /*判断指针是否有效*/ if (IS_ERR(rt)) { err = PTR_ERR(rt); if (err == -ENETUNREACH) //Network is unreachable IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES); return err; }
对于函数IS_ERR、PTR_ERR的具体介绍,请参见[Appendix:ERR].
/*TCP不能使用类型为组播或多播的路由缓存项*/ if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) { ip_rt_put(rt); return -ENETUNREACH; //Network is unreachable } /* 如果IP选项为空或者没有开启源路由功能,则采用查找到的缓存项 */ if (!inet_opt || !inet_opt->opt.srr) daddr = fl4->daddr; /* 如果没有设置源地址,则设置为缓存项中的源地址 */ if (!inet->inet_saddr) inet->inet_saddr = fl4->saddr; sk_rcv_saddr_set(sk, inet->inet_saddr); /* 如果该传输控制块的时间戳已被使用过,则重置各状态 rx_opt: tcp_options_received */ if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) { /* Reset inherited state */ /*下一个待发送的TCP段中的时间戳回显值*/ tp->rx_opt.ts_recent = 0; /*从接收到的段中取出时间戳*/ tp->rx_opt.ts_recent_stamp = 0; /*What does it means repair*/ if (likely(!tp->repair)) tp->write_seq = 0; } /* 在启用了tw_recycle:time wait recycle的情况下,重设时间戳 它用来快速回收TIME_WAIT连接. */ if (tcp_death_row.sysctl_tw_recycle && !tp->rx_opt.ts_recent_stamp && fl4->daddr == daddr) tcp_fetch_timewait_stamp(sk, &rt->dst); /* 设置传输控制块 */ inet->inet_dport = usin->sin_port; sk_daddr_set(sk, daddr); inet_csk(sk)->icsk_ext_hdr_len = 0; if (inet_opt) inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen; /* 设置MSS大小 */ tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT; /* Socket identity is still unknown (sport may be zero). * However we set state to SYN-SENT and not releasing socket * lock select source port, enter ourselves into the hash tables and * complete initialization after this. */ /* 将TCP的状态设置为SYN_SENT */ tcp_set_state(sk, TCP_SYN_SENT); err = inet_hash_connect(&tcp_death_row, sk); if (err) goto failure; sk_set_txhash(sk); /* 如果源端口或者目的端口发生改变,则需要重新查找路由, 并用新的路由缓存项更新sk中保存的路由缓存项。 */ rt = ip_route_newports(fl4, rt, orig_sport, orig_dport, inet->inet_sport, inet->inet_dport, sk); if (IS_ERR(rt)) { err = PTR_ERR(rt); rt = NULL; goto failure; } /* 将目的地址提交到套接字 */ sk->sk_gso_type = SKB_GSO_TCPV4; sk_setup_caps(sk, &rt->dst); /* 如果没有设置序号,则计算初始序号 序号与双方的地址与端口号有关系 */ if (!tp->write_seq && likely(!tp->repair)) tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr, inet->inet_daddr, inet->inet_sport, usin->sin_port); /* 计算IP首部的id域的值 全局变量jiffies用来记录自系统启动以来产生的节拍的总数 */ inet->inet_id = tp->write_seq ^ jiffies; /* 调用tcp_connect构造并发送SYN包*/ err = tcp_connect(sk); rt = NULL; if (err) goto failure; return 0;
总结起来,是在根据用户提供的目的地址, 设置好了传输控制块,为传输做好准备。如果在这一过程中出现错误,则会跳到 错误处理代码。
failure: /* 将状态设定为TCP_CLOSE,释放端口,并返回错误值。 */ tcp_set_state(sk, TCP_CLOSE); ip_rt_put(rt); sk->sk_route_caps = 0; inet->inet_dport = 0; return err;
tcp_connect
上面的会进行一系列的判断,之后真正构造SYN包的部分 被放在了中。接下来,我们来分析这个函数。/* Location: net/ipv4/tcp_output.c Function: 该函数用于构造并发送SYN包。 Parameter: sk:传输控制块。 */ int tcp_connect(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *buff; int err; /* 初始化tcp连接 */ tcp_connect_init(sk);
关于tcp_connect_init更多的内容,请参见[TCPInitialize:tcp_connect_init]。
if (unlikely(tp->repair)) { //what does it mean repair? /* 如果repair位被置1,那么结束TCP连接 */ tcp_finish_connect(sk, NULL); return 0; } /* 分配一个sk_buff */ buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true); if (unlikely(!buff)) return -ENOBUFS; //No buffer space available /* 初始化skb,并自增write_seq的值 */ tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
关于tcp_init_nondata_skb更多的内容,请参见[TCPInitialize:tcp_init_nondata_skb]。
/* 设置时间戳 */ tp->retrans_stamp = tcp_time_stamp; /* 将当前的sk_buff添加到发送队列中 */ tcp_connect_queue_skb(sk, buff); /* ECN (Explicit Congestion Notification,) 支持显式拥塞控制 */ tcp_ecn_send_syn(sk, buff); //what does this function do? /* 发送SYN包,这里同时还考虑了Fast Open的情况,意思就是 可以在连接建立的时候同时发数据。 */ err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) : tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); if (err == -ECONNREFUSED) /*Connection refused*/ return err; /* We change tp->snd_nxt after the tcp_transmit_skb() call * in order to make this packet get counted in tcpOutSegs. */ tp->snd_nxt = tp->write_seq; //send next 下一个待发送字节的序列号 /*pushed_seq means Last pushed seq, required to talk to windows???*/ tp->pushed_seq = tp->write_seq; TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS); /* 设定超时重传定时器 */ inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto, TCP_RTO_MAX); return 0; }
相关文章推荐
- Linux socket 初步
- Linux Kernel 4.0 RC5 发布!
- linux lsof详解
- linux 文件权限
- Linux 执行数学运算
- 10 篇对初学者和专家都有用的 Linux 命令教程
- Linux 与 Windows 对UNICODE 的处理方式
- Ubuntu12.04下QQ完美走起啊!走起啊!有木有啊!
- 解決Linux下Android开发真机调试设备不被识别问题
- 运维入门
- 运维提升
- Linux 自检和 SystemTap
- Ubuntu Linux使用体验
- c语言实现hashmap(转载)
- Linux 信号signal处理机制
- linux下mysql添加用户
- Scientific Linux 5.5 图形安装教程