- 浏览: 1403628 次
- 性别:
- 来自: 火星
文章分类
最新评论
-
aidd:
内核处理time_wait状态详解 -
ahtest:
赞一下~~
一个简单的ruby Metaprogram的例子 -
itiProCareer:
简直胡说八道,误人子弟啊。。。。谁告诉你 Ruby 1.9 ...
ruby中的类变量与类实例变量 -
dear531:
还得补充一句,惊群了之后,数据打印显示,只有一个子线程继续接受 ...
linux已经不存在惊群现象 -
dear531:
我用select试验了,用的ubuntu12.10,内核3.5 ...
linux已经不存在惊群现象
首先我们来看socket如何将一些状态的变化通知给对应的进程,比如可读,可写,出错等等。
先来看sock结构中这几个相关域:
这里我们一个个来说。
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这个等待队列上是否有元素。
我们每次唤醒用户进程之前都会调用这个函数进行判断,如果没有那就不用唤醒进程了。
先来看sock_def_wakeup,这个函数用来唤醒所有阻塞在当前的sock的进程。
然后是sk_data_ready,这个是用来发起可读事件的。
接下来是sock_def_write_space,它用来发起可写事件。
可以看到这个实现和前面的有所不同,它会先判断已提交的内存大小sk_wmem_alloc的2倍和sk的发送缓冲区(sk_sndbuf),如果大于sndbuf,则不要唤醒。这是因为为了防止太小的缓冲区导致每次写只能写一部分,从而效率太低。
最后是sock_def_error_report,他是当sock有错误时会被调用来唤醒相关的进程。
这里我们看到所有的处理最终都会调用sk_wake_async来处理异步的事件。这个函数其实比较简单,主要是处理如果我们在用户空间设置了相关的句柄的O_ASYNC属性时,也就是信号io,我们需要单独的处理信号io。
看完这些函数,我们再来看这些函数什么时候会被调用并且调用后如何被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最终会调用到这个函数:
然后是tcp_done,这个函数也就是在整个tcp连接完全断开或者说当前的sock没有和任何进程相关联(SOCK_DEAD)的状态,或者收到reset时,所做的工作。要注意这个函数不会free buffer的,它只是处理状态机的一些东西:
然后是tcp_fin函数,我们要重点来看这个函数,这个函数代表我们接受到了一个fin分节。
1 我们知道当服务器端收到一个fin后会,它会发送一个ack,然后通知应用程序,紧接着进入close_wait状态,等待应用程序的关闭。
2 当我们处于TCP_FIN_WAIT1状态(客户端)时,如果收到一个fin,会进入tcp_closing状态。
3 当我们处于TCP_FIN_WAIT2状态(客户端)时,如果收到fin,则会进入time_wait状态。
然后就是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来通知应用程序的。
来看代码片断:
可以看到只有当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,然后发送给应用程序不同的信号。
这里我们来看这几个错误号,可以看到
1 当TCP_SYN_SENT状态时如果收到rst,则会设置错误号为ECONNREFUSED,并唤醒进程。
2 当为TCP_CLOSE_WAIT状态时,则设置错误号为EPIPE。
3 剩余的状态的话错误号都为 ECONNRESET。
接下来我们就来看当读或者写已经收到rst的sock会出现什么情况。
先是写函数,前面的blog我们知道写函数是tcp_sendmsg。我们来看代码片断:
在看这个函数之前我们先来看sock_error函数,这个用来返回sk->sk_err,然后清除这个error。
然后我们来看sk_stream_wait_connect的实现。我们这里只看他的错误处理部分。
这里为什么要判断完err之后还要再次判断状态呢,等下面我们分析sk_stream_error函数之后一起来看。
然后来看sk_stream_error函数,要知道err这里传递进来的是wait_connect返回的值.
接下来我们把两个函数一起来看。
当我们收到一个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,也就是说当缓冲区有数据,我们的返回值为拷贝完的数据,而没有数据的话,直接会返回错误号。也就是说是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).
先来看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; }
发表评论
-
Receive packet steering patch详解
2010-07-25 16:46 12156Receive packet steering简称rp ... -
内核中拥塞窗口初始值对http性能的影响分析
2010-07-11 00:20 9708这个是google的人提出的 ... -
linux 内核tcp拥塞处理(一)
2010-03-12 16:17 9579这次我们来分析tcp的拥塞控制,我们要知道协议栈都是很保守的, ... -
内核tcp协议栈SACK的处理
2010-01-24 21:13 12185上一篇处理ack的blog中我 ... -
内核tcp的ack的处理
2010-01-17 03:06 11163我们来看tcp输入对于ack,段的处理。 先是ack的处理, ... -
内核处理time_wait状态详解
2010-01-10 17:39 6822这次来详细看内核的time_wait状态的实现,在前面介绍定时 ... -
linux内核sk_buff的结构分析
2009-12-25 00:42 47921我看的内核版本是2.6.32. 在内核中sk_buff表示一 ... -
tcp的输入段的处理
2009-12-18 00:56 8362tcp是全双工的协议,因此每一端都会有流控。一个tcp段有可能 ... -
内核协议栈tcp层的内存管理
2009-11-28 17:13 12089我们先来看tcp内存管理相关的几个内核参数,这些都能通过pro ... -
linux内核定时器的实现
2009-10-31 01:44 10197由于linux还不是一个实时的操作系统,因此如果需要更高精度, ... -
linux内核中tcp连接的断开处理
2009-10-25 21:47 10342我们这次主要来分析相关的两个断开函数close和shotdow ... -
linux内核tcp的定时器管理(二)
2009-10-05 20:52 5432这次我们来看后面的3个定时器; 首先是keep alive定 ... -
linux内核tcp的定时器管理(一)
2009-10-04 23:29 9838在内核中tcp协议栈有6种 ... -
linux 内核tcp接收数据的实现
2009-09-26 20:24 14529相比于发送数据,接收数据更复杂一些。接收数据这里和3层的接口是 ... -
linux 内核tcp数据发送的实现
2009-09-10 01:41 19795在分析之前先来看下SO_RCVTIMEO和SO_SNDTIME ... -
tcp connection setup的实现(三)
2009-09-03 00:34 5206先来看下accept的实现. 其实accept的作用很简单, ... -
tcp connection setup的实现(二)
2009-09-01 00:46 8440首先来看下内核如何处理3次握手的半连接队列和accept队列( ... -
tcp connection setup的实现(一)
2009-08-23 04:10 5827bind的实现: 先来介绍几个地址结构. struct ... -
linux内核中socket的实现
2009-08-15 04:38 21110首先来看整个与socket相关的操作提供了一个统一的接口sys ... -
ip层和4层的接口实现分析
2009-08-08 03:50 6212首先来看一下基于3层的ipv4以及ipv6实现的一些4层的协议 ...
相关推荐
本文将分析五个开源的TCP/IP协议栈:BSD TCP/IP、uC/IP、LwIP、uIP以及TinyTcp,探讨它们的特点、适用场景以及选择考虑因素。 1、**BSD TCP/IP协议栈**: 源自Berkeley Software Distribution (BSD),它是其他商业...
### Linux TCP/IP 协议栈分析 #### 一、引言 随着互联网技术的发展,Linux作为最常用的开源操作系统之一,在网络通信领域扮演着至关重要的角色。Linux TCP/IP协议栈是实现网络通信的核心组件,深入理解其工作原理...
《Linux内核TCP/IP协议栈源码分析》 在深入探讨Linux内核的TCP/IP协议栈之前,我们先理解一下TCP/IP协议栈的基本结构。TCP/IP协议栈是互联网通信的核心,它将网络通信分为四层:应用层、传输层、网络层和数据链路层...
Linux TCP/IP协议栈源码分析文档是一份对Linux操作系统中TCP/IP协议栈源代码进行深入探讨的文档。文档以Linux2.6.18内核源码为基础,逐章节逐步分析了协议栈的各个子模块,从系统初始化到网络层、传输层的实现细节,...
在《Linux-TCPIP协议栈分析.pdf》这本书中,读者可以期待深入的源码分析,对每个层次的协议处理有更清晰的认识,同时了解如何调试和优化网络性能。这本书对于希望提升Linux网络编程能力或者进行内核开发的开发者来说...
本文对modbus TCP协议栈的实现进行了详细的分析和研究,介绍了基于Java技术的modbus TCP协议栈的构造,并结合了当前工业发展的需要,旨在实现远程监控工业机器与移动互联设备之间的互联。 一、Modbus协议简介 ...
《linux TCP IP协议栈源码解析.pdf》无疑是核心资料,它将带领读者逐行解读协议栈的C语言源代码,包括处理TCP连接建立、数据传输、断开连接,以及IP分片、路由选择等关键流程。 《LinuxKernelnetwork.pdf》可能包含...
TCP/IP协议栈的实现是一个复杂而精细的过程,涉及到网络数据的分段处理、错误检测与纠正、网络路径选择等多个环节。本篇将深入解析这些关键知识点,并结合压缩包中的源代码文件,探讨其实现细节。 首先,TCP...
本篇文章将基于“tcp-ip协议栈分析”这一主题,深入探讨Linux内核中TCP/IP协议栈的实现机制,包括其内部结构、关键组件以及数据流的处理流程。 #### Linux内核中的TCP/IP协议栈概览 Linux内核的TCP/IP协议栈主要...
在Linux 2.6.18内核版本中,协议栈源码的分析涵盖了操作系统与网络协议的基础知识,以及Linux内核的初始化过程、网络设备驱动、数据包的收发、协议处理等核心环节。书中首先介绍了Linux操作系统的基本架构,包括其...
51单片机TCP/IP协议栈ZLIP的源码提供了实现上述功能的具体代码,包括各个层次的协议处理函数、内存管理函数、定时器管理函数等。通过阅读和理解源码,可以了解协议栈的工作原理,为实际项目开发提供参考。 总结,51...
在这个简单的TCP协议栈中,ARP的实现可能包含解析和响应 ARP 请求,以及缓存 IP 地址到 MAC 地址的映射,以提高后续通信效率。 接着是ICMP(Internet控制消息协议),它是TCP/IP协议族的一部分,用于在IP网络中传输...
在μCOS-Ⅱ中实现TCP/IP协议栈,可以为嵌入式设备提供网络功能,使其具备访问Internet的能力,支持各种网络应用,如远程控制、数据传输等。 TCP/IP协议栈通常由四层模型构成:应用层、传输层、网络层和链路层。在μ...
读者可以通过阅读,了解TCP协议栈如何处理各种网络状况,如何调整参数以优化性能,以及如何调试和解决相关问题。 总的来说,《Linux内核源码剖析—TCP/IP实现 下册》是一本深入探讨TCP协议栈技术的书籍,对于提升对...
- `tcpdump`用于抓取网络包,分析协议栈运行情况。 - `strace`跟踪系统调用,查看应用程序与内核的交互。 - `netstat`显示网络状态,如连接状态、监听端口等。 通过深入学习Linux TCP/IP协议栈源码,开发者可以...
本篇内容将重点放在Linux 2.6.35内核中的TCP/IP协议栈源代码上,并详细分析其核心数据结构和关键处理过程。 首先,我们要了解在Linux网络子系统中,数据包的处理是以socket buffer,即sk_buff结构体为核心的。这个...
压缩包中的“TCP.doc”文件可能包含了详细的TCP/IP协议栈分析,包括TCP协议的设置、连接管理、拥塞控制、错误检测和纠正等方面。文件可能还涵盖了Linux内核中的TCP选项配置、性能调优方法以及如何通过系统调用来操作...
嵌入式TCP/IP协议栈的设计旨在使单片机能够处理复杂的网络通信任务,如数据封装、分组转发、错误检测和处理等。 3. 单片机嵌入式TCP/IP协议栈的设计方法: 设计单片机嵌入式TCP/IP协议栈需要硬件和软件的紧密结合,...