一、粘包问题
TCP传输是一种基于流(stream)的传输方式,这个流是对应于udp的数据报格式的传输方式。在数据报传输格式中,每次传输的是一个单位,可以认为他是一个离散的信号,每次发送一个报单位。而流则是细水长流的流,它的数据可以持续的发送,接收和发送端都可以认为其中的字节流是没有天然的分隔点的,不能抽刀断水水更流。
具体在接收的时候,如果有一个udp报文到来,用户态接收的buffer小于报文真正的大小,此时超过通过recv接收提供的参数之外的内核接收数据在此次系统调用之后丢失,而tcp中,如果用户态提供的buffer小于一个包的实际载荷,剩余内容会被重新放回报文中,供下次接收。之前一片文章大致讨论过这个问题。
此时的一个问题就是当使用tcp的时候,假设发送的报文比较小,例如我们C语言中的一个结构,但是悲剧的是它可以发送各种不同的结构,而结构的类型是通过一个开始的type来标志,此时两个报文在发送方就可能被合并,因为流的概念就是源源不断,没有定界。另一方面,如果接收方提供的缓冲区大于一个结构报文的大小,那么内核同样会等待下一个包到来之后拼凑够了足够的空间才会从用户态返回。这些行为在用户态看来都是一些所谓的“粘包”现象。
二、tcp发送时并报
这个最为闻名的就是nagle算法,该算法是为了解决网络上可能出现的大量的小包问题。因为一个IP报文正常负载可以达到1.5K左右,如果对于telnet之类的交互式设备,它每次只发送一个字符,这个字符同样要占用一个IP报文,报文到来和发送都是需要经过网卡、经过带宽、经过中断,加之TCPheader,Ipheader、mac address等信息,这个字符的发送还是很不划算的。所以nagle的实现方法就是如果有小包的话,我就直接发送,但是不是每个都发送,如果说我发送了一个小包,但是这个包还没有被回应,此时新的小包就不再发送,而是等小包回来之后再发送这个报文。在大家很多时候做telnet或者ssh操作的时候可能经常会看到串口不响应,但是过一点时间同时跳出多个字符,此时就是nagle累计了多个小包之后统一发送的报文。
内核中的nagle算法实现
static inline void tcp_minshall_update(struct tcp_sock *tp, int mss,
const struct sk_buff *skb)
{
if (skb->len < mss)小包的定义,就是小于一个mss。
tp->snd_sml = TCP_SKB_CB(skb)->end_seq;
}
static inline int tcp_minshall_check(const struct tcp_sock *tp)
{
return after(tp->snd_sml,tp->snd_una) &&
!after(tp->snd_sml, tp->snd_nxt);
}
/* Return 0, if packet can be sent now without violation Nagle's rules:
* 1. It is full sized.
* 2. Or it contains FIN. (already checked by caller)
* 3. Or TCP_NODELAY was set.
* 4. Or TCP_CORK is not set, and all sent packets are ACKed.
* With Minshall's modification: all sent small packets are ACKed.
*/
static inline int tcp_nagle_check(const struct tcp_sock *tp,
const struct sk_buff *skb,
unsigned mss_now, int nonagle)
{
return (skb->len < mss_now &&
((nonagle&TCP_NAGLE_CORK) ||
(!nonagle &&
tp->packets_out &&
tcp_minshall_check(tp))));这里是我们最为常见的形式,就是使能了nagle算法,有一些包已经发送出去,但是之后的
}
三、ack的延时发送
为了节省带宽,响应方同样会避免单独对一个包含数据的报文发送纯粹的ack报文,而是通过携带的方式来确认。因为在很多情况下,接收的业务方会比较快的响应这个报文,而此时我们就可以在这个响应的业务报文中传递一个确认报文,反正这个位置是TCP header中常驻信息,所以不用也是浪费。
所以内核中在接收到一个有有效负载的报文(出啦syn、fin、ack之外还有数据)之后会尝试不马上使用一个纯粹的ack报文来完成进行确认,而是等待有相应报文是让该报文捎带确认数据。但是这里有一个问题,就是如果接收方没有发送相应数据呢?这个延迟的报文岂不是要一直被delay?
所以此时解决方法同样简单,那就是设置一个合理的定时器,如果说在定时器超时之后还是没有人来捎带这个数据,那么就只能专门分配一个确认报文了。
static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{
struct tcp_sock *tp = tcp_sk(sk);
/* More than one full frame received... */
if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss这里是否立即响应同样是有一个常常的判断条件,这里不再细说。因为我暂时用不到,看起来比较耗时间。但是和nagle算法一样,用户态可以通过套接字的选项来使这个条件满足。
/* ... and right edge of window advances far enough.
* (tcp_recvmsg() will send ACK otherwise). Or...
*/
&& __tcp_select_window(sk) >= tp->rcv_wnd) ||
/* We ACK each frame or... */
tcp_in_quickack_mode(sk) ||
/* We have out of order data. */
(ofo_possible &&
skb_peek(&tp->out_of_order_queue))) {
/* Then ack it now */
tcp_send_ack(sk);
} else {
/* Else, send delayed ack. */
tcp_send_delayed_ack(sk);
}
}
static inline void tcp_ack_snd_check(struct sock *sk)
{
if (!inet_csk_ack_scheduled(sk)) {
/* We sent a data segment already. */
return;
}
__tcp_ack_snd_check(sk, 1);
}
大部分是走到else中,启动定时器。
四、延迟ack何时被取消
这里有个问题,就是如果不能捎带,那么定时超时,此时直接发送确认报文,这个是没什么好说的,但是如果碰巧被人捎带了的话,此时这个定时器在什么地方被取消的呢?
在发送一个tcp报文的时候,内核执行的代码为
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask)
if (likely(tcb->flags & TCPCB_FLAG_ACK))如果ACK标志被置位,则清空定时器。
tcp_event_ack_sent(sk, tcp_skb_pcount(skb));
static inline void tcp_event_ack_sent(struct sock *sk, unsigned int pkts)
{
tcp_dec_quickack_mode(sk, pkts);
inet_csk_clear_xmit_timer(sk, ICSK_TIME_DACK);
}
在用户态通常是会设置套接口的NoBlock属性,此时read/recv系统调用就是有多少读多少,通常是一个报文,这个报文小于mss。
五、tty设备read的特殊性
在tty设备中,read传入的参数无论有多大,默认情况下,只要读到换行符,这个系统调用就会返回。可能是因为tty当初设计的时候就是为了进行和用户交互,而用户通常键入回车就是希望这个命令被执行了,所以此时从read返回可能是比较合理的。
static ssize_t read_chan(struct tty_struct *tty, struct file *file,
unsigned char __user *buf, size_t nr)
minimum = time = 0;
timeout = MAX_SCHEDULE_TIMEOUT;
if (!tty->icanon) {对于通常的串口来说,这个分支是不满足的,所以minimun的值为零。
time = (HZ / 10) * TIME_CHAR(tty);
minimum = MIN_CHAR(tty);
if (minimum) {
if (time)
tty->minimum_to_wake = 1;
else if (!waitqueue_active(&tty->read_wait) ||
(tty->minimum_to_wake > minimum))
tty->minimum_to_wake = minimum;
} else {
timeout = 0;
if (time) {
timeout = time;
time = 0;
}
tty->minimum_to_wake = minimum = 1;
}
}
……
while (nr) {
……
if (tty->icanon) {
/* N.B. avoid overrun if nr == 0 */
while (nr && tty->read_cnt) {
……
eol = test_and_clear_bit(tty->read_tail,
tty->read_flags);此处的标志位在下一节中的 handle_newline标签处设置。
……
if (eol)
break;
……
if (b - buf >= minimum)只要读到eol,此处可以退出返回。
break;
if (time)
timeout = time;
}
六、tty换行时唤醒
static inline void n_tty_receive_char(struct tty_struct *tty, unsigned char c)
handle_newline:
spin_lock_irqsave(&tty->read_lock, flags);
set_bit(tty->read_head, tty->read_flags);
put_tty_queue_nolock(c, tty);
tty->canon_head = tty->read_head;
tty->canon_data++;
spin_unlock_irqrestore(&tty->read_lock, flags);
kill_fasync(&tty->fasync, SIGIO, POLL_IN);
if (waitqueue_active(&tty->read_wait))
wake_up_interruptible(&tty->read_wait);
return;