tcp协议栈处理各种事件的分析

首先我们来看socket如何将一些状态的变化通知给对应的进程,比如可读,可写,出错等等。

先来看sock结构中这几个相关域:

struct sock {
..........................
wait_queue_head_t *sk_sleep;
.....................................
void (*sk_state_change)(struct sock *sk);
void (*sk_data_ready)(struct sock *sk, int bytes);
void (*sk_write_space)(struct sock *sk);
void (*sk_error_report)(struct sock *sk);
int (*sk_backlog_rcv)(struct sock *sk,struct sk_buff *skb);
};


这里我们一个个来说。

sk_sleep是一个等待队列,也就是所有阻塞在这个sock上的进程,我们通知用户进程就是通过这个等待队列来做的。

sk_state_change是一个sock状态改变的回调函数,也就是当sock的状态变迁了(比如从established到clos_wait状态),那么就会调用这个函数。

sk_data_ready 这个函数是当当前sock有可读数据的时候,就会被调用。

sk_write_space 这个函数是当当前的sock有可写的空间的时候,就会被调用。

sk_error_report 这个函数是当当前的sock出错(比如收到一个rst)后就会被调
用.

接下来我们就来看这几个函数的具体实现。

在看之前我们先来看一个sk_has_sleeper的实现,它的作用就是用来判断是否有进程阻塞在当前的sock,也就是sk_slleep这个等待队列上是否有元素。

我们每次唤醒用户进程之前都会调用这个函数进行判断,如果没有那就不用唤醒进程了。


static inline int sk_has_sleeper(struct sock *sk)
{
smp_mb__after_lock();
///判断等待队列是否有元素。
return sk->sk_sleep && waitqueue_active(sk->sk_sleep);
}


先来看sock_def_wakeup,这个函数用来唤醒所有阻塞在当前的sock的进程。


static void sock_def_wakeup(struct sock *sk)
{
read_lock(&sk->sk_callback_lock);
///先检测是否有进程阻塞在当前的sock上,如果有才会去唤醒等待队列。
if (sk_has_sleeper(sk))
wake_up_interruptible_all(sk->sk_sleep);
read_unlock(&sk->sk_callback_lock);
}


然后是sk_data_ready,这个是用来发起可读事件的。



static void sock_def_readable(struct sock *sk, int len)
{
read_lock(&sk->sk_callback_lock);
///首先判断是否有进程休眠在sock上。如果有则同步唤醒所有的阻塞的进程,这里注意传递的参数是POLLIN,这样我们就能通过epoll这类来捕捉事件了。
if (sk_has_sleeper(sk))
wake_up_interruptible_sync_poll(sk->sk_sleep, POLLIN | POLLRDNORM | POLLRDBAND);
///这里主要是处理异步的唤醒事件。
sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
read_unlock(&sk->sk_callback_lock);
}


接下来是sock_def_write_space,它用来发起可写事件。

可以看到这个实现和前面的有所不同,它会先判断已提交的内存大小sk_wmem_alloc的2倍和sk的发送缓冲区(sk_sndbuf),如果大于sndbuf,则不要唤醒。这是因为为了防止太小的缓冲区导致每次写只能写一部分,从而效率太低。



static void sock_def_write_space(struct sock *sk)
{
read_lock(&sk->sk_callback_lock);

/* Do not wake up a writer until he can make "significant"
* progress. --DaveM
*/
///先判断内存使用。
if ((atomic_read(&sk->sk_wmem_alloc) << 1) <= sk->sk_sndbuf) {
///判断是否有需要被唤醒的进程。
if (sk_has_sleeper(sk))
///同步的唤醒进程,可以看到事件是POLLOUT.
wake_up_interruptible_sync_poll(sk->sk_sleep, POLLOUT |
POLLWRNORM | POLLWRBAND);
///这里也是处理一些异步唤醒。
if (sock_writeable(sk))
sk_wake_async(sk, SOCK_WAKE_SPACE, POLL_OUT);
}

read_unlock(&sk->sk_callback_lock);
}


最后是sock_def_error_report,他是当sock有错误时会被调用来唤醒相关的进程。


static void sock_def_error_report(struct sock *sk)
{
read_lock(&sk->sk_callback_lock);

///这里只需要注意上报的事件是POLL_ERR。
if (sk_has_sleeper(sk))
wake_up_interruptible_poll(sk->sk_sleep, POLLERR);
///处理异步的唤醒。
sk_wake_async(sk, SOCK_WAKE_IO, POLL_ERR);
read_unlock(&sk->sk_callback_lock);
}


这里我们看到所有的处理最终都会调用sk_wake_async来处理异步的事件。这个函数其实比较简单,主要是处理如果我们在用户空间设置了相关的句柄的O_ASYNC属性时,也就是信号io,我们需要单独的处理信号io。



int sock_wake_async(struct socket *sock, int how, int band)
{
if (!sock || !sock->fasync_list)
return -1;
///通过how的不同来进行不同的处理,这里每次都要先测试flags的状态。
switch (how) {
case SOCK_WAKE_WAITD:
if (test_bit(SOCK_ASYNC_WAITDATA, &sock->flags))
break;
goto call_kill;
case SOCK_WAKE_SPACE:
if (!test_and_clear_bit(SOCK_ASYNC_NOSPACE, &sock->flags))
break;
/* fall through */
case SOCK_WAKE_IO:
call_kill:
///发送信号给应用进程。(SIGIO)
__kill_fasync(sock->fasync_list, SIGIO, band);
break;
case SOCK_WAKE_URG:
///这里是发送urgent信号(SIGURG)给应用进程。
__kill_fasync(sock->fasync_list, SIGURG, band);
}
return 0;
}


看完这些函数,我们再来看这些函数什么时候会被调用并且调用后如何被select,epoll这类的框架所捕捉。

先来看sk_state_change(也就是sock_def_wakeup),直接搜索内核代码。可以看到有6个地方调用了sk_state_change函数,分别是inet_shutdown,tcp_done,tcp_fin,tcp_rcv_synsent_state_process以及tcp_rcv_state_process函数。可以看到这些基本都是tcp状态机的的状态变迁的地方,也就是每次状态的变迁都会调用state_change函数。

我们一个个来看,有些可能前面已经分析过了,这里就简要介绍下。先是inet_shutdown.

这个函数我们知道系统调用shutdown最终会调用到这个函数:



int inet_shutdown(struct socket *sock, int how)
{
struct sock *sk = sock->sk;
.......................................
///唤醒等待的进程。
sk->sk_state_change(sk);
release_sock(sk);
return err;
}


然后是tcp_done,这个函数也就是在整个tcp连接完全断开或者说当前的sock没有和任何进程相关联(SOCK_DEAD)的状态,或者收到reset时,所做的工作。要注意这个函数不会free buffer的,它只是处理状态机的一些东西:



void tcp_done(struct sock *sk)
{
if (sk->sk_state == TCP_SYN_SENT || sk->sk_state == TCP_SYN_RECV)
TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_ATTEMPTFAILS);

///最终状态机回到初始状态(也就是接到最后一个ack).
tcp_set_state(sk, TCP_CLOSE);
///清理定时器
tcp_clear_xmit_timers(sk);
///设置shutdown的状态。
sk->sk_shutdown = SHUTDOWN_MASK;

///如果还有进程和它相关联则调用state_change来唤醒相关进程。
if (!sock_flag(sk, SOCK_DEAD))
sk->sk_state_change(sk);
else
///否则destroy这个sock
inet_csk_destroy_sock(sk);
}


然后是tcp_fin函数,我们要重点来看这个函数,这个函数代表我们接受到了一个fin分节。

1 我们知道当服务器端收到一个fin后会,它会发送一个ack,然后通知应用程序,紧接着进入close_wait状态,等待应用程序的关闭。

2 当我们处于TCP_FIN_WAIT1状态(客户端)时,如果收到一个fin,会进入tcp_closing状态。

3 当我们处于TCP_FIN_WAIT2状态(客户端)时,如果收到fin,则会进入time_wait状态。


static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
{
struct tcp_sock *tp = tcp_sk(sk);


inet_csk_schedule_ack(sk);

sk->sk_shutdown |= RCV_SHUTDOWN;
///设置sock的状态。
sock_set_flag(sk, SOCK_DONE);

///不同的状态进入不同的处理
switch (sk->sk_state) {
///syn_recv状态和establish状态的处理是相同的。
case TCP_SYN_RECV:
case TCP_ESTABLISHED:
/* Move to CLOSE_WAIT */
///直接进入close_wait状态
tcp_set_state(sk, TCP_CLOSE_WAIT);
///设置pingpong,也就是这个时候所有数据包都是立即ack
inet_csk(sk)->icsk_ack.pingpong = 1;
break;
///处于这两个状态,说明这个fin只不过是个重传数据包。
case TCP_CLOSE_WAIT:
case TCP_CLOSING:

break;
case TCP_LAST_ACK:
/* RFC793: Remain in the LAST-ACK state. */
break;

///我们在等待fin,
case TCP_FIN_WAIT1:
///因此我们先发送ack,给对端,然后设置状态为closing状态。
tcp_send_ack(sk);
tcp_set_state(sk, TCP_CLOSING);
break;

///这个状态说明我们已经接到ack,在等待最后的fin。
case TCP_FIN_WAIT2:
///发送ack给对方,
tcp_send_ack(sk);
//然后进入time-wait状态,并启动定时器。
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
break;
default:
///其他的状态收到都是错误的。
printk(KERN_ERR "%s: Impossible, sk->sk_state=%d\n",
__func__, sk->sk_state);
break;
}

///可以看到一接到fin,就会马上清理调ofo队列。
__skb_queue_purge(&tp->out_of_order_queue);
if (tcp_is_sack(tp))
tcp_sack_reset(&tp->rx_opt);
sk_mem_reclaim(sk);

///这里判断是否有进程和sock关联。
if (!sock_flag(sk, SOCK_DEAD)) {
///ok,现在通知进程状态的改变。
sk->sk_state_change(sk);
///这里注意如果是两端只有一端关闭,则是不会发送poll_hup而是发送poll_in也就是可读事件(这里也只是处理信号io的东西)
if (sk->sk_shutdown == SHUTDOWN_MASK ||
sk->sk_state == TCP_CLOSE)
sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_HUP);
else
sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
}
}


然后就是tcp_rcv_synsent_state_process函数,这个函数主要用来处理syn_sent状态的数据。这个函数前面的blog已经介绍过了,这里就不介绍了,这里它会在接受到ack之后调用state_change函数,也就是要进入established状态。

接下来就是tcp_rcv_state_process函数中的两个调用的地方,第一个地方是当处于TCP_SYN_RECV状态然后接受到一个ack,此时需要进入established状态,则会调用state_change函数.

这里要注意的是第二个地方,那就是当处于TCP_FIN_WAIT1,然后接受到了一个ack,此时由于应用程序调用close时,设置了linger套接口选项,因此我们这里有可能需要唤醒休眠等待的进程。

接下来的可读和可写的回调函数我就简单的介绍下,先来看可读函数sk_data_ready。它会在收到urgent数据后马上调用(tcp_urg中).还有就是dma中,再就是tcp_rcv_established中,我们主要来看这个,因为这个是我们最主要的接收函数。

这里要知道我们是处于软中断上下文中,然后调用data_ready来通知应用程序的。
来看代码片断:


if (eaten)
__kfree_skb(skb);
else
sk->sk_data_ready(sk, 0);


可以看到只有当eaten为0时才会唤醒等待队列。并且这段代码是处于fast path中的。而eaten为0说明拷贝给用户空间失败。

然后是sk_write_space函数,这个函数主要是在tcp_new_space中以及sock_wfree中被调用。

这两个函数第一个是当接收到一个ack之后,我们能从write_queue中删除这条报文的时候被调用。

第二个函数是当free写buff的时候被调用。


接下来来详细看sk_error_report 回调函数,这个函数,我们主要来看两个调用它的地方,一个是tcp_disconnect中,一个是tcp_reset中。

第一个函数是用来断开和对端的连接。第二个函数是处理rst分节的。

第一个函数就不介绍了,我前面的blog已经分析过了,这个函数被调用,是当我们自己调用close的时候或者说当前的sock要被关闭的时候会调用这个函数。

来看第二个函数,也就是处理rst分节的部分。

这里可以看到是先设置错误号,也就是防止应用程序读写错误的sock,然后发送给应用程序不同的信号。
static void tcp_reset(struct sock *sk)
{
///不同的状态设置不同的错误号
switch (sk->sk_state) {
case TCP_SYN_SENT:
sk->sk_err = ECONNREFUSED;
break;
case TCP_CLOSE_WAIT:
sk->sk_err = EPIPE;
break;
case TCP_CLOSE:
return;
default:
///其他状态都是ECONNRESET。
sk->sk_err = ECONNRESET;
}

///传递错误给应用程序。
if (!sock_flag(sk, SOCK_DEAD))
sk->sk_error_report(sk);

tcp_done(sk);
}


这里我们来看这几个错误号,可以看到

1 当TCP_SYN_SENT状态时如果收到rst,则会设置错误号为ECONNREFUSED,并唤醒进程。

2 当为TCP_CLOSE_WAIT状态时,则设置错误号为EPIPE。

3 剩余的状态的话错误号都为 ECONNRESET。

接下来我们就来看当读或者写已经收到rst的sock会出现什么情况。

先是写函数,前面的blog我们知道写函数是tcp_sendmsg。我们来看代码片断:


///如果是established或者close_wait状态则进入wait_connect处理,我们上面可以看到(tcp_reset)接收到rst后,我们会通过tcp_done设置状态为tcp_close,所以我们一定会进入这里。
if ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT))
///这里不等于0也就是表示sk_err有值。接下来会详细分析这个函数。
if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
goto out_err;

..................................
...............................................
out_err:
///调用这个函数进行处理
err = sk_stream_error(sk, flags, err);



在看这个函数之前我们先来看sock_error函数,这个用来返回sk->sk_err,然后清除这个error。



static inline int sock_error(struct sock *sk)
{
int err;
///如果为空则直接返回。
if (likely(!sk->sk_err))
return 0;
///这个是用汇编实现的。返回sk_err然后设置sk_err为0.也就是清空。
err = xchg(&sk->sk_err, 0);
//返回错误号,这里主要加了个负号。
return -err;
}


然后我们来看sk_stream_wait_connect的实现。我们这里只看他的错误处理部分。


int sk_stream_wait_connect(struct sock *sk, long *timeo_p)
{
struct task_struct *tsk = current;
DEFINE_WAIT(wait);
int done;

do {
///首先取得sk_err,然后清空sk_err
int err = sock_error(sk);
///如果err存在则直接返回。
if (err)
return err;
//到达这里说明err是0,如果状态不是sent或者recv则返回-EPIPE.
if ((1 << sk->sk_state) & ~(TCPF_SYN_SENT | TCPF_SYN_RECV))
return -EPIPE;
.......................................

} while (!done);
return 0;
}

这里为什么要判断完err之后还要再次判断状态呢,等下面我们分析sk_stream_error函数之后一起来看。

然后来看sk_stream_error函数,要知道err这里传递进来的是wait_connect返回的值.


int sk_stream_error(struct sock *sk, int flags, int err)
{
///如果传递进来的err是-EPIPE,进入第一个处理。
if (err == -EPIPE)
//如果sk_err没有被清掉,则返回sk_err否则返回-EPIPE.
err = sock_error(sk) ? : -EPIPE;
//如果是EPIPE的话,会直接发送信号给应用程序
if (err == -EPIPE && !(flags & MSG_NOSIGNAL))
send_sig(SIGPIPE, current, 0);
///返回错误号
return err;
}


接下来我们把两个函数一起来看。

当我们收到一个rst之后我们设置状态为tcp_close.然后设置相应的错误号。

假设现在我们在收到rst的sock上调用send方法,此时我们会进入sk_stream_wait_connect,然后直接返回err。现在就有3种情况:

1 我们在TCP_SYN_SENT状态收到的rst,此时的错误号为ECONNREFUSED,因此我们会返回ECONNREFUSED,设置err为ECONNREFUSED,然后进入sk_stream_error处理。由于我们的err并不等于EPIPE,因此我们会直接返回错误号。也就是应用进程回收到错误号。不过此时sk_err已经清0了。

2 当我们在TCP_CLOSE_WAIT收到rst,此时的错误号为EPIPE,因此我们进入sk_stream_error的时候,err就是EPIPE,此时我们就会发送EPIPE信号给进程,并返回EPIPE的错误号。

3 我们在其他状态收到rst,此时错误号为ECONNRESET,可以看到这个的处理和上面TCP_SYN_SENT状态收到的rst的处理一致,只不过返回的错误号不一样罢了。

紧接着,我们来看当我们第一次调用完毕之后,第二次调用会发生什么。这里可以看到只有上面的1,3两种情况第二次调用才会有效果。

当我们再次进入sk_stream_wait_connect,此时由于sk_err已经被清0,因此我们会进入第二个处理也就是状态判断,由于我们是tcp_close状态,因此我们会直接返回-EPIPE.此时我们出来之后,会再次进入sk_stream_error,而之后调用就和上面的2 一样了。会直接发送epipe信号,然后返回epipe.


然后是tcp_recvmsg,也就是接收函数。

下面是代码片断。这里要知道,就算收到rst,如果sock没有完全关闭,我们还是可以从缓冲区读取数据的。

这个函数我前面的blog已经介绍过了。详细的介绍可以看我前面的blog。


//copied为已经复制的数据。这里可以看到如果已经复制了一些数据,并且sk_err有值,则直接跳出循环。
if (copied) {
if (sk->sk_err ||
sk->sk_state == TCP_CLOSE ||
(sk->sk_shutdown & RCV_SHUTDOWN) ||!timeo ||signal_pending(current))
break;
} else {
if (sock_flag(sk, SOCK_DONE))
break;
///如果没有值,可以看到cpoied直接被设置为错误号。
if (sk->sk_err) {
copied = sock_error(sk);
break;
}
///到这里copied为0
if (sk->sk_shutdown & RCV_SHUTDOWN)
break;


这个函数最终的返回值是copied,也就是说当缓冲区有数据,我们的返回值为拷贝完的数据,而没有数据的话,直接会返回错误号。也就是说是ECONNREFUSED,EPIPE或者ECONNRESET。而当再次调用,还是没有数据的话,会直接返回0.

最后我们来看tcp_poll的实现,这个函数也就是当sock的等待队列有事件触发式会被调用的。有关slect和epoll的源码分析可以看这个:

http://docs.google.com/Doc?docid=0AZr7tK22PNlAZGRqdHZ4NHFfMTQ0Zmh0OTIzZzQ&hl=en

http://docs.google.com/Doc?docid=0AZr7tK22PNlAZGRqdHZ4NHFfMTQ2ZnIzMmtwaHM&hl=en

ok,我们来看tcp_poll.它的功能很简单,就是得到触发的事件掩码,然后返回给
select,poll或者epoll。

我们来看代码。这里要知道shutdown域,这个域主要是通过shutdown来设置。不过其他地方偶尔也会设置,比如收到fin(见上面的tcp_fin).


unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
unsigned int mask;
struct sock *sk = sock->sk;
struct tcp_sock *tp = tcp_sk(sk);

sock_poll_wait(file, sk->sk_sleep, wait);
///如果是tcp_listen的话,单独处理。
if (sk->sk_state == TCP_LISTEN)
return inet_csk_listen_poll(sk);

mask = 0;
//如果sk_err有设置则添加POLLERR事件。
if (sk->sk_err)
mask = POLLERR;


//如果shutdown被设置,或者tcp状态为close,则添加POLLHUP
if (sk->sk_shutdown == SHUTDOWN_MASK || sk->sk_state == TCP_CLOSE)
mask |= POLLHUP;
///如果shutdown被设置为RCV_SHUTDOWN则添加 POLLIN | POLLRDNORM | POLLRDHUP状态(我们通过上面知道,当接收到fin后就会设置为RCV_SHUTDOWN)。
if (sk->sk_shutdown & RCV_SHUTDOWN)
mask |= POLLIN | POLLRDNORM | POLLRDHUP;

///如果不是TCPF_SYN_SENT以及TCPF_SYN_RECV状态。
if ((1 << sk->sk_state) & ~(TCPF_SYN_SENT | TCPF_SYN_RECV)) {
int target = sock_rcvlowat(sk, 0, INT_MAX);

if (tp->urg_seq == tp->copied_seq &&
!sock_flag(sk, SOCK_URGINLINE) &&
tp->urg_data)
target--;


if (tp->rcv_nxt - tp->copied_seq >= target)
mask |= POLLIN | POLLRDNORM;
///如果没有设置send_shutdown.则进入下面的处理。
if (!(sk->sk_shutdown & SEND_SHUTDOWN)) {

///如果可用空间大于最小的buf,则添加POLLOUT | POLLWRNORM事件。
if (sk_stream_wspace(sk) >= sk_stream_min_wspace(sk)) {
mask |= POLLOUT | POLLWRNORM;
} else { /* send SIGIO later */

//设置flag的标记位
set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);

//再次判断,因为有可能此时又有空间了。
if (sk_stream_wspace(sk) >= sk_stream_min_wspace(sk))
mask |= POLLOUT | POLLWRNORM;
}
}
//如果有紧急(urgent)数据,添加POLLPRI事件。
if (tp->urg_data & TCP_URG_VALID)
mask |= POLLPRI;
}
return mask;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值