6.3 紧急模式

  通常情况下,TCP数据由连接的一端发送到另一端,数据的所有字节都是精确排序的,后写入的字节绝不会早于先写入的字节到达。然而socket API提供一种功能可以使得一些数据无阻的先于早写入的数据到达接收端,这种功能就是所谓的发送带外数据 。在TCP中这样的功能是通过紧急数据模式完成的。

  一个TCP流通常希望顺序地发送数据字节,那么乱序的发送数据就似乎与流的概念相违背。那么为什么要提供带外数据的实现方法呢?

  考虑一种情况:一个TCP发送端有一个大量的数据排队等待发送到网络中。在接收端,也会有大量已接收的,却还没有被应用进程读取的数据 。如果数据发送端进程由于一些原因需要取消想接收端传输数据的请求,那么它就需要向接收端紧急发送一个标识取消的请求。如果这个取消请求传输的不及时,那么就会浪费一些接收端的资源。

  使用带外数据的实际程序例子就是telnet,ftp命令。telnet会将中止字符作为紧急数据发送到对端。这会允许对端清除所有未处理的输入数据,并且丢弃所有未发送的终端输出。ftp命令使用带外数据来中断一个文件的传输。

  本文关注的是Linux内核中TCP紧急数据模式的实现原理。

6.3.1 API使用方法

  收发带外数据的流程与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数据。

6.3.2 内核实现

  发送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 <pall@rz.uni-karlsruhe.de>
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会将紧急数据当作普通数据处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
vdhcoapp6.3是指一个软件或应用程序的版本号。根据版本号中的数字,我们可以推断这是该软件或应用程序的第六个版本的第三个分支或更新。版本号的每个部分都有特定的含义,它们被用来表示软件的更新和改进。通常,当开发者对软件进行了一些改动或增加新功能时,他们会发布新的版本。在该版本号中,"vdhcoapp"可能是软件或应用程序的名称的缩写,而"6.3"则代表了具体的版本号。 由于没有具体提及软件或应用程序的名称和功能,所以很难对vdhcoapp6.3进行具体的解释。但是,一般而言,软件的更新版本通常会包括对先前版本中的错误和漏洞进行修复,改进用户界面和性能,增加新的功能和功能改进等。可以推测,在vdhcoapp6.3中,开发者可能对先前版本中的一些问题进行了修复,并可能引入了一些新的功能或改进了现有的功能。 对于具体的功能和特性,我们需要查阅软件或应用程序的相关文档或开发者的说明。通过了解软件或应用程序的名称和用途,我们可以更加详细地了解vdhcoapp6.3的具体含义和作用。 总之,vdhcoapp6.3是一个软件或应用程序的版本号,代表该软件或应用程序的第六个版本的第三个分支或更新。根据版本号,我们可以推测该版本可能包括了对先前版本中的问题进行修复,以及新的功能或改进的引入。为了更深入地了解其功能和特性,需要查阅详细的相关文档或开发者的说明。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值