Chinaunix首页 | 论坛 | 博客
  • 博客访问: 16867
  • 博文数量: 3
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 10
  • 用 户 组: 普通用户
  • 注册时间: 2014-12-09 16:21
文章分类
文章存档

2015年(3)

我的朋友
最近访客

分类: LINUX

2015-10-22 22:28:21

原文地址:Linux socket中缓冲区 作者:buaa_zhaoc

之前在前面的Linux socket缓冲区引起的死锁博客中讲述了这个具体的死锁过程。当时也没有很仔细的看Linux内部的实现代码,也没有具体看内部是如何实现的。这两天没事的时候看了两眼代码,找到了对应的实现方式。

为了说明后续的实现过程,首先需要介绍Linux内部为每个socket所维护的一个struct sock这样一个对象,socket相当与一个统一的接口,那么sock就相当与一个具体的实现。其中,包括一个链接所应有的信息。由于这个结构比较庞大,这篇博客中只介绍与缓冲区相关的内容。与缓冲区相关的内容如下所示 在include/net/sock.h中
  1. struct sock
  2. {
  3.     /* 其他字段 */
  4.     int sk_rcvbuf;            /* 接受缓冲区大小 */
  5.     atomic_t sk_rmem_alloc;   /* 已经申请的read memory */
  6.     atomic_t sk_wmem_alloc;   /* 已经申请的write memory */
  7.     int sk_sndbuf;            /* 发送缓冲区大小 */
  8.     /* 其他字段 */
  9. };
以上就是一个socket中缓冲区维护的相关内容。

网络数据流在系统中是这样的一个方向,上层应用在发送的时候主动获得sk_buff,而接受所获得的包则是通过网卡来申请所获的。

协议栈要在发送一个数据包的时候需要调用
  1. struct sk_buff *sock_alloc_send_skb(struct sock *sk, unsigned long size,int noblock, int *errcode)
来获得一个网络buffer。这个函数调用sock_alloc_send_pskb,下面救主要来分析sock_alloc_send_pskb(net/core/sock.c)这个函数,然后得到关于发送缓冲区维护的相关内容。
  1. struct sk_buff *sock_alloc_send_pskb(struct sock *sk, unsigned long header_len,
  2.                  unsigned long data_len, int noblock,
  3.                  int *errcode)
  4. {
  5.     /* 获得超时时间,如果是非阻塞的,超时时间是0 */
  6.     timeo = sock_sndtimeo(sk, noblock);
  1.     while (1) {
  2.         /* 检查socket 是否失败 */
  3.         err = sock_error(sk);
  4.         if (err != 0)
  5.             goto failure;

  6.         /* 这里赋值Broken pipe, 在没有读之前出现shutdown,这就是出现broken pipe的地方 */
  7.         err = -EPIPE;
  8.         if (sk->sk_shutdown & SEND_SHUTDOWN)
  9.             goto failure;
  10.         /* 
  11.            这里检测已经申请的写内存是否有超出发送缓冲区,如果是超过发送缓冲区,那么需要进行睡眠,等待
  12.            有其他的线程释放这个socket的写内存,如果是非阻塞的话,那么次函数就返回EAGAIN, 
  13.         */
  14.         if (atomic_read(&sk->sk_wmem_alloc) < sk->sk_sndbuf) {
  15.             /* 申请一个sk_buff结构,用来容下所需的发送内容 */
  16.             skb = alloc_skb(header_len, gfp_mask);
  17.             if (skb) {
  18.                 int npages;
  19.                 int i;

  20.                 /* No pages, we're done... */
  21.                 if (!data_len)
  22.                     break;
  23.                 /* 计算所需的内存页面数量 */
  24.                 npages = (data_len + (PAGE_SIZE - 1)) >> PAGE_SHIFT;
  25.                 skb->truesize += data_len;
  26.                 skb_shinfo(skb)->nr_frags = npages;
  27.                 for (i = 0; i < npages; i++) {
  28.                     /* 逐条申请每个页面,如果此时有页面申请失败,则返回ENOBUF */
  29.                 }
  30.                 /* 完全成功后,跳出这个循环,准备返回 */
  31.                 /* Full success... */
  32.                 break;
  33.             }
  34.             err = -ENOBUFS;
  35.             goto failure;
  36.         }
  37.         set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
  38.         set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
  39.         /* 如果是非阻塞的,那么返回EAGAIN */
  40.         err = -EAGAIN;
  41.         if (!timeo)
  42.             goto failure;
  43.         /* 如果此时有信号,那么跳到相应处理,如:返回EINTR等 */
  44.         if (signal_pending(current))
  45.             goto interrupted;
  46.         /* 这里是进行pendding, 等待其他线程释放对应的写内存 */
  47.         timeo = sock_wait_for_wmem(sk, timeo);
  48.     }
  49.     /* 设定sk_buff的owner为sk,成功返回 */
  50.     skb_set_owner_w(skb, sk);
  51.     return skb;

  52. interrupted:
  53.     err = sock_intr_errno(timeo);
  54. failure:
  55.     *errcode = err;
  56.     return NULL;
  57. }
从上面的示意代码中能够看出,系统在每次发送数据包前会检测已经发送的数据量,如果已经申请还未发送的数据超过了设定的发送缓冲区大小,那么就会阻塞住发送线程,或者返回EAGAIN。

同样在接收包时通过sock_rmalloc申请sk_buff,或者通过sock_queue_rcv_skb将sk_buff接入到指定的相应socket接受队列时,都需要检测sk_rcvbuf的大小与sk_rmem_alloc之间的关系,如果没有足够可用的rcvbuf那么则选择将收到的包丢弃,以防止过多的数据包停留在内核中,耗空系统资源。


除此外通过getsockopt与setsockopt能够获得和改变相应的接收缓冲区大小以及发送缓冲区大小。附上实验程序小代码,代码如下:
  1. #include <stdio.h>
  2. #include <errno.h>
  3. #include <sys/types.h>
  4. #include <sys/socket.h>

  5. int main()
  6. {
  7.     int sock_fd = -1;
  8.     int snd_buf_size = 0;
  9.     socklen_t opt_size = sizeof(snd_buf_size);
  10.     int ret = 0;

  11.     sock_fd = socket(AF_INET, SOCK_STREAM, 0);
  12.     if (sock_fd < 0)
  13.     {
  14.         perror("socket fail");
  15.         goto out;
  16.     }
  17.     /* 获得sndbuf的长度 */
  18.     ret = getsockopt(sock_fd, SOL_SOCKET, SO_SNDBUF, &snd_buf_size, &opt_size);
  19.     if (ret < 0)
  20.     {
  21.         perror("getsockopt fail");
  22.         printf("%d\n", errno);
  23.         goto out;
  24.     }
  25.     printf("socket %d's sndbuf is %d bytes\n", sock_fd, snd_buf_size);

  26.     /* 修改sndbuf的长度 */
  27.     snd_buf_size = 10000;
  28.     ret = setsockopt(sock_fd, SOL_SOCKET, SO_SNDBUF, &snd_buf_size, opt_size);
  29.     if (ret < 0)
  30.     {
  31.         perror("getsockopt fail");
  32.         printf("%d\n", errno);
  33.         goto out;
  34.     }

  35.     /* 再获得sndbuf的长度 */
  36.     ret = getsockopt(sock_fd, SOL_SOCKET, SO_SNDBUF, &snd_buf_size, &opt_size);
  37.     if (ret < 0)
  38.     {
  39.         perror("getsockopt fail");
  40.         printf("%d\n", errno);
  41.         goto out;
  42.     }
  43.     printf("socket %d's sndbuf is %d bytes\n", sock_fd, snd_buf_size);
  44. out:
  45.     if (sock_fd >= 0)
  46.     {
  47.         close(sock_fd);
  48.         sock_fd = 0;
  49.     }
  50.     return 0;
  51. }
运行程序结果如下
  1. socket 3's sndbuf is 16384 bytes
  2. socket 3's sndbuf is 20000 bytes
上述程序很简单,有一点需要说明的是,在程序中设置的是10000,但是在内核中却乘以了2,这个至于为什么我也没有追究过,以后有机会在追踪吧。

总的来说,内核中维护缓冲区大小就是通过4个整数之间的关系搞定,在申请之前检测相应的值,如果能阻塞的阻塞,不能阻塞的放弃。由于收包都是硬件搞定,不能阻塞,也不能让其重试,所以只能选择丢弃。发包的话,可以根据write是否为阻塞,来进行相应策略。

阅读(1739) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~