linux中TCP的socket、bind、listen、connect和accept的实现

来源:互联网 发布:ida远程调试linux 编辑:程序博客网 时间:2024/05/18 01:02

首先介绍和服务器端相关的系统调用,依次为socket->bind->listen->accept

socket:

当服务器程序调用socket系统调用之后,内核会创建一个struct socket和一个struct sock结构,两者可以通过指针

成员变量相互访问对方。内核直接操作的是struct sock结构。struct socket的存在是为了适应linux的虚拟文件系统,

把socket也当作一个文件系统,通过指定superblock中不同的操作函数实现完成相应的功能。

在linux内核中存在不同的sock类型,与TCP相关的有struct sock、 struct inet_connection_sock,、struct tcp_sock等。

这些结构的实现非常灵活,可以相互进行类型转换。这个机制的实现是结构体的一层层包含关系:struct tcp_sock的

第一个成员变量是struct inet_connection_sock,struct inet_connection_sock的第一个成员变量是struct sock。

通过这种包含关系,可以将不同的sock类型通过指针进行相互转换。比如:

struct tcp_sock tcp_sk; struct sock *sk = (struct sock *)&tcp_sk;

不同类型的sock的包含关系如下:
 


为了避免从小的结构体转换到大的结构体造成内存越界,对于TCP协议,内核在初始化一个stuct sock时给它分配的空间

大小是一个struct tcp_sock的大小。这样sock类型的相互转换便可以灵活的进行。

另外,在内核创建完sock和socket之后,还需要绑定到对应的文件描述符以便应用层能够访问。一个task_struct中有一个

文件描述符数组,存储所有该进程打开的文件,因为socket也可以看做是文件,也存储在这个数组中。文件描述符就是

该socket在该数组中的下标,具体的实现请参照虚拟文件系统。


bind:

创建完socket之后就是地址的绑定了通过bind系统调用实现。该调用通过传递进来的文件描述符找到对应的socket

结构然后通过socket访问sock结构。操作sock进行地址的绑定。如果指定了端口检查端口的可用性并绑定否则随

机分配一个端口进行绑定。但是怎样获知当前系统的端口绑定状态呢?通过一个全局变量inet_hashinfo进行每次成

功绑定一个端口会都将该sock加入到inet_hashinfo的绑定散列表中。加入之后bind的系统调用已基本完成了。


listen:

接下来是listen系统调用,该过程比较复杂。和listen相关的大部分信息存储在inet_connection_sock结构中。同样的

内核通过文件描述符找到对应的sock然后将其转换为inet_connection_sock结构。在inet_connection_sock结构体

中含有一个类型为request_sock_queueicsk_accept_queue变量,存储一些希望建立连接的sock相关的信息。

结构为:

struct request_sock_queue {struct request_sock *rskq_accept_head;struct request_sock *rskq_accept_tail;rwlock_t syn_wait_lock;u8rskq_defer_accept;struct listen_sock *listen_opt;};



listen_opt用了存储当前正在请求建立连接的sock,称作半连接状态,用request_sock表示。request_sock有个成员

变量指针指向对应的strut sock。rskq_accept_head和rskq_accept_tail分别指向已经建立完连接的request_sock,称

全连接状态,这些sock都是完成了三次握手等待程序调用accept接受。程序调用listen之后会为icsk_accept_queue

分配内存,并且将当前的监听sock放到全局变量inet_hashinfo中的监听散列表中。

当内核收到一个带有skb之后会通过tcp_v4_rcv函数进行处理。因为只有skb,还需找到对应的sock。该过程通过

__inet_lookup_skb进行实现。该函数主要调用__inet_lookup,其中:

1.首先看看这个包是不是一个已经建立好连接的sock中的包通过__inet_lookup_established函数进行操作

(一个连接通过源IP,目的IP,源PORT和目的PORT标识)。
2.失败的话可能是一个新的SYN数据包
此时还没有建立连接所以没有对应的sock和该sock相关的只可能是监听sock了。

所以通过__inet_lookup_listener函数找到在本地的监听对应端口的sock。

无论哪种情况找到sock之后便会将sock和skb一同传入tcp_v4_do_rcv函数作统一处理

if (sk->sk_state == TCP_ESTABLISHED) {sock_rps_save_rxhash(sk, skb->rxhash);TCP_CHECK_TIMER(sk);if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {rsk = sk;goto reset;}TCP_CHECK_TIMER(sk);return 0;}if (sk->sk_state == TCP_LISTEN) {struct sock *nsk = tcp_v4_hnd_req(sk, skb);if (!nsk)goto discard;if (nsk != sk) {if (tcp_child_process(sk, nsk, skb)) {rsk = nsk;goto reset;}return 0;}}if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {rsk = sk;goto reset;}


1.如果是一个已建立连接的sock调用tcp_rcv_established函数进行相应的处理。
2.如果是一个正在监听的sock
需要新建一个sock来保存这个半连接请求该操作通过tcp_v4_hnd_req实现。
这里我们只关注tcp的建立过程
所以只分析tcp_v4_hnd_req和tcp_child_process函数

static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb){struct tcphdr *th = tcp_hdr(skb);const struct iphdr *iph = ip_hdr(skb);struct sock *nsk;struct request_sock **prev;struct request_sock *req = inet_csk_search_req(sk, &prev, th->source,       iph->saddr, iph->daddr);if (req)return tcp_check_req(sk, skb, req, prev);nsk = inet_lookup_established(sock_net(sk), &tcp_hashinfo, iph->saddr,th->source, iph->daddr, th->dest, inet_iif(skb));if (nsk) {if (nsk->sk_state != TCP_TIME_WAIT) {bh_lock_sock(nsk);return nsk;}inet_twsk_put(inet_twsk(nsk));return NULL;}return sk;}


1.首先调用inet_csk_search_req查找在半连接队列中是否已经存在对应的request_sock。有的话说明这个请求

连接已经存在,调用tcp_check_req处理第三次握手的情况,当sock的状态从SYN_RCV变迁到ESTABLISHED

态时连接建立完成。需要将该request_sock从request_sock_queue队列中的listen_opt半连接队列取出

放入全连接队列等待进程调用accept取走同时是request_sock指向一个新建的sock并返回。

2.没有的话调用inet_lookup_established从已经建立连接sock中查找,如果找到的话说明这是一条已经建立的连

接,当该sock不处于timewait将sock返回状态时将sock返回,否则返回NULL。

3.当上述两种情况都失败了,表示这是一个新的为创建的连接,直接返回sk。


这样通过tcp_v4_hnd_req函数就能够找到或创建和这个skb相关的sock。


A.如果返回的sock和处于Listen状态的sock不同表示返回的是一个新的sock第三次握手已经完成了。调用

tcp_child_process处理。该函数的逻辑让这个新的tcp_sock开始处理TCP段,同时唤醒应用层调用accept阻

塞的程序,告知它有新的请求建立完成,可以从全连接队列中取出了。

B.如果返回的sock没有变化表示是一个新的请求调用tcp_rcv_state_process函数处理第一次连接的情况。

该函数的逻辑较为复杂简单的可以概括为新建一个request_sock并插入半连接队列设置该request_sock的

sock为SYN_RCV状态。然后构建SYN+ACK发送给客户端完成TCP三次握手连接的第二步。


accept:

最后的是accept系统调用。该调用创建新的struct socket表示新的连接搜寻全连接队列如果队列为空将程

序自身挂起等待连接请求的完成。否则从队列中取出头部request_sock并设置新的struct socket和request_sock

中的struct sock的对应关系。这样一个连接至此就建立完成了。客户端可以通过新返回的struct socket进行通信

同时旧的struct socket继续在监听。


接下来介绍客户端的操作,客户端依次调用的是socket->connect

socket:

socket的实现和服务器端一样,不再复述了。

connect:

connect系统调用根据文件描述符找到socket和sock,如果当前socket的状态时SS_UNCONNECTED的情况下才

正常处理连接请求。首先调用tcp协议簇的connect函数(即tcp_v4_connect)发送SYN,然后将socket状态置为

SS_CONNECTING,将进程阻塞等待连接的完成。剩下的两次握手由协议栈自动完成。

tcp_v4_connect函数:

该函数首先进行一些合法性验证,随后调用ip_route_connect函数查找路由信息,将当前sock置为SYN_SENT状态,

然后调用inet_hash_connect函数绑定本地的端口,和服务器端绑定端口的过程类似,但是会额外的将sock添加

inet_hashinfo中的ehash散列表中(添加到这的原因是因为希望以后收到的SYN+ACK时能够找到对应的sock,

虽然当前并没有真正意义上的建立连接)。到最后调用tcp_connect构建SYN数据包发送。

tcp_connect:

该函数逻辑比较简单,构造SYN数据段并设置相应的字段,将该段添加到发送队列上调用tcp_transmit_skb发送skb,

最后重设重传定时器以便重传SYN数据段。


当客户端收到SYN+ACK之后,首先会通过tcp_v4_rcv从已建立连接散列表中找到对应的sock,然后调用tcp_v4_do_rcv

函数进行处理,在该函数中主要的执行过程是调用tcp_rcv_state_process。

又回到了tcp_rcv_state_process函数,它处理sock不处于ESTABLISHED和LISTEN的所有情况。当发现是一个SYN+ACK

段并且当前sock处于SYN_SENT状态时,表示连接建立马上要完成了。首先初始化TCP连接中一些需要的信息,如窗口大小,

MSS,保活定时器等信息。然后给该sock的上层应用发送信号告知它连接建立已经完成,最后通过tcp_send_ack发送ACK

完成最后一次握手。

0 0