通常情况下,TCP数据由连接的一端发送到另一端,数据的所有字节都是精确排序的,后写入的字节绝不会早于先写入的字节到达。然而socket API提供一种功能可以使得一些数据无阻的先于早写入的数据到达接收端,这种功能就是所谓的发送带外数据 。在TCP中这样的功能是通过紧急数据模式完成的。
一个TCP流通常希望顺序地发送数据字节,那么乱序的发送数据就似乎与流的概念相违背。那么为什么要提供带外数据的实现方法呢?
考虑一种情况:一个TCP发送端有一个大量的数据排队等待发送到网络中。在接收端,也会有大量已接收的,却还没有被应用进程读取的数据 。如果数据发送端进程由于一些原因需要取消想接收端传输数据的请求,那么它就需要向接收端紧急发送一个标识取消的请求。如果这个取消请求传输的不及时,那么就会浪费一些接收端的资源。
使用带外数据的实际程序例子就是telnet,ftp命令。telnet会将中止字符作为紧急数据发送到对端。这会允许对端清除所有未处理的输入数据,并且丢弃所有未发送的终端输出。ftp命令使用带外数据来中断一个文件的传输。
本文关注的是Linux内核中TCP紧急数据模式的实现原理。
收发带外数据的流程与C/S模式基本一致,不同之处在于发送带外数据时需要使用增加了OOB标记的send函数:
send(sockfd, buf, buf_len, MSG_OOB);这样buf[buf_len - 1]会被设置为带外数据。
对于server端,需要增加捕捉紧急信号的函数,并在函数中使用增加了OOB标记的recv函数,另外还需要使用fcntl将socket设置为当前进程所有:
14 static void sigurg (int signo) 15 { 16 int n; 17 char buf[256] = {}; 18 19 n = recv(connfd, buf, sizeof(buf), MSG_OOB); 20 if (n < 0) { 21 printf("OOB recv failed\n"); 22 return; 23 } 24 25 printf("URG data is %s (%d) /n",buf,n); 26 } 28 int 29 main (int argc, char **argv) 30 { ... 59 if (signal(SIGURG ,sigurg) < 0) { 60 printf ("signal catch error: %s(errno: %d)\n", strerror (errno), errno); 61 exit (0); 62 } ... 66 if ((connfd = accept (listenfd, (struct sockaddr *) NULL, NULL)) == -1) 67 { 68 printf ("accept socket error: %s(errno: %d)", strerror (errno), errno); 69 continue; 70 } 71 fcntl(connfd, F_SETOWN, getpid()); 72 n = recv (connfd, buff, MAXLINE, 0); ...在server端在内核收到OOB数据时会发送SIGURG信号给进程,这样进程就会在信号处理函数中优先收到一个字节的OOB数据。
发送OOB数据时tcp_sendmsg函数的处理如下:
1016 int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, 1017 size_t size) 1018 { ... 1029 flags = msg->msg_flags; //带有MSG_OOB标记 ... 1201 if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair)) //如果有OOB标记则先不发送数据,直到全部数据都写入发送队列 1202 continue; ... 1214 if (copied) 1215 tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH); ...tcp_push函数:
613 static inline void tcp_mark_urg(struct tcp_sock *tp, int flags) 614 { 615 if (flags & MSG_OOB) 616 tp->snd_up = tp->write_seq; //记录紧急指针为要写入的下一个字节的seq 617 } 618 619 static inline void tcp_push(struct sock *sk, int flags, int mss_now, 620 int nonagle) 621 { 622 if (tcp_send_head(sk)) { 623 struct tcp_sock *tp = tcp_sk(sk); 624 625 if (!(flags & MSG_MORE) || forced_push(tp)) 626 tcp_mark_push(tp, tcp_write_queue_tail(sk)); 627 628 tcp_mark_urg(tp, flags); 629 __tcp_push_pending_frames(sk, mss_now, 630 (flags & MSG_MORE) ? TCP_NAGLE_CORK : nonagle); 631 } 632 }tcp_transmit_skb函数在构建TCP报头时会设置紧急指针的值和标记位:
828 static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, 829 gfp_t gfp_mask) 830 { ... 915 if (unlikely(tcp_urg_mode(tp) && before(tcb->seq, tp->snd_up))) { //处于紧急模式且紧急数据尚未发送 916 if (before(tp->snd_up, tcb->seq + 0x10000)) { //snd_up < seq + 65536,即没有超出16 bit的snd_up能够表示的范围 917 th->urg_ptr = htons(tp->snd_up - tcb->seq); //紧急指针表示紧急数据与当前seq的偏移 918 th->urg = 1; //设置紧急标记位 919 } else if (after(tcb->seq + 0xFFFF, tp->snd_nxt)) { //seq + 65535 > snd_nxt 920 th->urg_ptr = htons(0xFFFF); //设置偏移为最大 921 th->urg = 1; //设置紧急标记位 922 } 923 } ...915:如果紧急数据尚未发送并接收完成则tcp_urg_mode会为真:
374 static inline bool tcp_urg_mode(const struct tcp_sock *tp) 375 { 376 return tp->snd_una != tp->snd_up; 377 }
919-921:到这里则紧急数据的偏移会大于65535;如果seq + 65535 > snd_nxt,可以将紧急指针设为最大,这样可以告知收数据端有紧急数据要到来,但由于紧急指针的偏移太大会使接收端意识到紧急数据尚未收到,从而做出相应处理。
TCP会在慢速路径中用tcp_urg函数处理紧急数据:
4799 static void tcp_check_urg(struct sock *sk, const struct tcphdr *th) 4800 { 4801 struct tcp_sock *tp = tcp_sk(sk); 4802 u32 ptr = ntohs(th->urg_ptr); 4803 4804 if (ptr && !sysctl_tcp_stdurg) //紧急指针非空且没有设置net.ipv4.tcp_stdurg为1 4805 ptr--; //设置紧急指针指向紧急数据 4806 ptr += ntohl(th->seq); //得到紧急数据的序列号 4807 4808 /* Ignore urgent data that we've already seen and read. */ 4809 if (after(tp->copied_seq, ptr)) //忽略已经读取完毕的紧急数据 4810 return; ... 4822 if (before(ptr, tp->rcv_nxt)) //紧急指针非法 4823 return; 4824 4825 /* Do we already have a newer (or duplicate) urgent pointer? */ 4826 if (tp->urg_data && !after(ptr, tp->urg_seq)) //再次收到紧急指针且新的紧急数据的序列号不比之前的大 4827 return; 4828 4829 /* Tell the world about our new urgent pointer. */ 4830 sk_send_sigurg(sk); //发送SIGURG信号给进程 ... 4847 if (tp->urg_seq == tp->copied_seq && tp->urg_data && //下一个要copy的字节就是紧急数据且紧急数据尚未读取,这意味着有旧的紧急数据未读 4848 !sock_flag(sk, SOCK_URGINLINE) && tp->copied_seq != tp->rcv_nxt) {//没有设置以内联方式读取紧急数据且读缓存中没有比紧急数据更新的数据 4849 struct sk_buff *skb = skb_peek(&sk->sk_receive_queue); 4850 tp->copied_seq++; //越过紧急数据,该数据会被新的紧急数据覆盖 4851 if (skb && !before(tp->copied_seq, TCP_SKB_CB(skb)->end_seq)) { //数据最后一个字节也copy完毕 4852 __skb_unlink(skb, &sk->sk_receive_queue); //将skb移出接收队列 4853 __kfree_skb(skb); //释放skb 4854 } 4855 } 4856 4857 tp->urg_data = TCP_URG_NOTYET; //标记收到紧急数据 4858 tp->urg_seq = ptr; //记录紧急数据序列号 4859 4860 /* Disable header prediction. */ 4861 tp->pred_flags = 0; 4862 } 4863 4864 /* This is the 'fast' part of urgent handling. */ 4865 static void tcp_urg(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th) 4866 { 4867 struct tcp_sock *tp = tcp_sk(sk); 4868 4869 /* Check if we get a new urgent pointer - normally not. */ 4870 if (th->urg) 4871 tcp_check_urg(sk, th); //检查紧急数据 4872 4873 /* Do we wait for any urgent data? - normally not... */ 4874 if (tp->urg_data == TCP_URG_NOTYET) { //当前包中有紧急指针 4875 u32 ptr = tp->urg_seq - ntohl(th->seq) + (th->doff * 4) - 4876 th->syn; //计算紧急数据在包内的偏移 4877 4878 /* Is the urgent pointer pointing into this packet? */ 4879 if (ptr < skb->len) { //紧急数据在当前skb中 4880 u8 tmp; 4881 if (skb_copy_bits(skb, ptr, &tmp, 1)) //copy全部紧急数据(1字节)到tmp中 4882 BUG(); 4883 tp->urg_data = TCP_URG_VALID | tmp; //将紧急数据记录在tp->urg_data中,同时清除TCP_URG_NOTYET标记 4884 if (!sock_flag(sk, SOCK_DEAD)) 4885 sk->sk_data_ready(sk, 0); //通告进程收数据 4886 } 4887 } 4888 }</span></span>4804-4805:TCP的实现在紧急数据的位置上有两种不同的解释:RFC793解释和BSD解释。缺省情况下,Linux紧急指针字段使用与BSD相兼容,即指针指向紧急数据后的第一个字节(即第一个非紧急数据)。在 RFC973 协议中是指向紧急数据的最后一个字节。net.ipv4.tcp_stdurg内核选项为0时使用BSD解释,否则使用RFC解释。不够由tcp_transmit_skb函数可以看出,在发送紧急数据时Linux TCP是使用的BSD解释。
4847-4857:紧急数据只有一个字节,保存在tp->urg_data中;如果应用进程以带外方式收取紧急数据但并不及时的话,旧的紧急数据会被新的紧急数据覆盖,从而造成数据丢失。
由上可见紧急数据会越过最先的数据(tp->rcv_nxt)被放入缓存中等待应用进程接收。
接下来内核会把包含紧急数据的skb放入接收缓存,并设置tp->rcv_nxt = end_seq,然后发送ACK确认数据。紧急数据发送者收到ACK后会调用tcp_clean_rtx_queue清除紧急数据:
3001 static int tcp_clean_rtx_queue(struct sock *sk, int prior_fackets, 3002 u32 prior_snd_una) 3003 { ... 3089 if (likely(between(tp->snd_up, prior_snd_una, tp->snd_una))) 3090 tp->snd_up = tp->snd_una; ...这时tcp_urg_mode就会为假,从而结束紧急模式。
TCP读取紧急数据的模式有两种:内联(Inline)和带外(Out-of-band)。默认的方式是带外。如果应用进程希望以带外的方式读取紧急数据,则需要在使用读包系统调用时设置MSG_OOB标记。使用内联方式则可以直接从字节流中读取紧急数据,不需要设置MSG_OOB标记。
进程收到SIGURG信号时如果空闲,可以直接使用recv系统调用收取紧急数据;如果正在执行recv系统调用,则可能被信号打断。下面来看紧急数据收包处理:
1545 int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, 1546 size_t len, int nonblock, int flags, int *addr_len) 1547 { ... 1570 if (flags & MSG_OOB) //带外方式收取紧急数据 1571 goto recv_urg; ... 1618 do { 1619 u32 offset; 1620 1621 /* Are we at urgent data? Stop if we have read anything or have SIGURG pending. */ 1622 if (tp->urg_data && tp->urg_seq == *seq) { //有紧急指针且下一个要copy的字节就是紧急数据 1623 if (copied) //已经copy了至少1字节数据 1624 break; //已经读了一些数据,马上就要读到紧急数据了,立马停住,以免将紧急数据混在普通数据中 1625 if (signal_pending(current)) { //有信号等待处理,可能有是在紧急数据到达慢速处理路径时TCP发送给进程的SIGURG信号 1626 copied = timeo ? sock_intr_errno(timeo) : -EAGAIN; //设置返回值 1627 break; //跳出循环,系统调用返回去处理SIGURG信号 1628 } 1629 } ... 1809 if (tp->urg_data) { 1810 u32 urg_offset = tp->urg_seq - *seq; 1811 if (urg_offset < used) {//紧急数据在能copy的数据中 1812 if (!urg_offset) {//下一个要copy的字节就是紧急数据 1813 if (!sock_flag(sk, SOCK_URGINLINE)) {//没有设置以inline方式读取紧急数据 1814 ++*seq;//跳过紧急数据 1815 urg_hole++; 1816 offset++; 1817 used--; 1818 if (!used) 1819 goto skip_copy; 1820 } 1821 } else 1822 used = urg_offset;//copy到紧急数据为止 1823 } 1824 } ... 1873 skip_copy: 1874 if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) { //紧急数据copy完毕 1875 tp->urg_data = 0; //清空紧急数据 1876 tcp_fast_path_check(sk); //重启快速处理路径 1877 } ... 1938 out: 1939 release_sock(sk); 1940 return err; 1941 1942 recv_urg: 1943 err = tcp_recv_urg(sk, msg, len, flags); 1944 goto out; ...
用户以带外方式读取紧急数据时由tcp_recv_urg进行处理:
1255 static int tcp_recv_urg(struct sock *sk, struct msghdr *msg, int len, int flags) 1256 { 1257 struct tcp_sock *tp = tcp_sk(sk); 1258 1259 /* No URG data to read. */ 1260 if (sock_flag(sk, SOCK_URGINLINE) || !tp->urg_data || 1261 tp->urg_data == TCP_URG_READ) 1262 return -EINVAL; /* Yes this is right ! */ 1263 1264 if (sk->sk_state == TCP_CLOSE && !sock_flag(sk, SOCK_DONE)) 1265 return -ENOTCONN; 1266 1267 if (tp->urg_data & TCP_URG_VALID) { //有紧急数据 1268 int err = 0; 1269 char c = tp->urg_data; //取tp->urg_data的低8位,这就是紧急数据 1270 1271 if (!(flags & MSG_PEEK)) 1272 tp->urg_data = TCP_URG_READ; //标识紧急数据已经读取 1273 1274 /* Read urgent data. */ 1275 msg->msg_flags |= MSG_OOB; //告知应用进程这是紧急数据 1276 1277 if (len > 0) { 1278 if (!(flags & MSG_TRUNC)) 1279 err = memcpy_toiovec(msg->msg_iov, &c, 1); //将紧急数据copy到用户缓存 1280 len = 1; 1281 } else 1282 msg->msg_flags |= MSG_TRUNC; 1283 1284 return err ? -EFAULT : len; 1285 } 1286 1287 if (sk->sk_state == TCP_CLOSE || (sk->sk_shutdown & RCV_SHUTDOWN)) 1288 return 0; 1289 1290 /* Fixed the recv(..., MSG_OOB) behaviour. BSD docs and 1291 * the available implementations agree in this case: 1292 * this call should never block, independent of the 1293 * blocking state of the socket. 1294 * Mike <[email protected]> 1295 */ 1296 return -EAGAIN; 1297 }RFC6093不建议新的应用程序使用TCP紧急数据机制,主要原因是:
(1)不同的TCP实现对紧急指针的理解是不一样的(如使用net.ipv4.tcp_stdurg就会改变Linux对紧急指针的理解),这会导致紧急数据机制很难拥有一个统一的应用环境
(2)一些中间数据传递设备(入侵检测系统)会去掉紧急指针。
另外,Linux TCP也没有可靠的机制能准确告知应用进程紧急数据的到来,也无法保证带外紧急数据能被应用进程接收。因为紧急数据是保存在tp->urg_data的低8位中,如果紧急数据第一次到来时没有被应用进程收取,很快第二个紧急数据到来时就会将第一个紧急数据覆盖掉,导致数据丢失。
RFC6093建议新的应用程序如果要使用TCP紧急数据机制则应该设置SO_OOBINLINE socke选项,使用内联方式收取紧急数据,这样TCP会将紧急数据当作普通数据处理。