转载出处:http://blog.chinaunix.net/uid-23146151-id-3125031.html
之前在前面的Linux socket缓冲区引起的死锁博客中讲述了这个具体的死锁过程。当时也没有很仔细的看Linux内部的实现代码,也没有具体看内部是如何实现的。这两天没事的时候看了两眼代码,找到了对应的实现方式。
为了说明后续的实现过程,首先需要介绍Linux内部为每个socket所维护的一个struct sock这样一个对象,socket相当与一个统一的接口,那么sock就相当与一个具体的实现。其中,包括一个链接所应有的信息。由于这个结构比较庞大,这篇博客中只介绍与缓冲区相关的内容。与缓冲区相关的内容如下所示 在include/net/sock.h中
- struct sock
- {
- /* 其他字段 */
- int sk_rcvbuf; /* 接受缓冲区大小 */
- atomic_t sk_rmem_alloc; /* 已经申请的read memory */
- atomic_t sk_wmem_alloc; /* 已经申请的write memory */
- int sk_sndbuf; /* 发送缓冲区大小 */
- /* 其他字段 */
- };
以上就是一个socket中缓冲区维护的相关内容。
网络数据流在系统中是这样的一个方向,上层应用在发送的时候主动获得sk_buff,而接受所获得的包则是通过网卡来申请所获的。
协议栈要在发送一个数据包的时候需要调用
- 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)这个函数,然后得到关于发送缓冲区维护的相关内容。
- struct sk_buff *sock_alloc_send_pskb(struct sock *sk, unsigned long header_len,
- unsigned long data_len, int noblock,
- int *errcode)
- {
- /* 获得超时时间,如果是非阻塞的,超时时间是0 */
- timeo = sock_sndtimeo(sk, noblock);
- while (1) {
- /* 检查socket 是否失败 */
- err = sock_error(sk);
- if (err != 0)
- goto failure;
-
- /* 这里赋值Broken pipe, 在没有读之前出现shutdown,这就是出现broken pipe的地方 */
- err = -EPIPE;
- if (sk->sk_shutdown & SEND_SHUTDOWN)
- goto failure;
- /*
- 这里检测已经申请的写内存是否有超出发送缓冲区,如果是超过发送缓冲区,那么需要进行睡眠,等待
- 有其他的线程释放这个socket的写内存,如果是非阻塞的话,那么次函数就返回EAGAIN,
- */
- if (atomic_read(&sk->sk_wmem_alloc) < sk->sk_sndbuf) {
- /* 申请一个sk_buff结构,用来容下所需的发送内容 */
- skb = alloc_skb(header_len, gfp_mask);
- if (skb) {
- int npages;
- int i;
-
- /* No pages, we're done... */
- if (!data_len)
- break;
- /* 计算所需的内存页面数量 */
- npages = (data_len + (PAGE_SIZE - 1)) >> PAGE_SHIFT;
- skb->truesize += data_len;
- skb_shinfo(skb)->nr_frags = npages;
- for (i = 0; i < npages; i++) {
- /* 逐条申请每个页面,如果此时有页面申请失败,则返回ENOBUF */
- }
- /* 完全成功后,跳出这个循环,准备返回 */
- /* Full success... */
- break;
- }
- err = -ENOBUFS;
- goto failure;
- }
- set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);
- set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
- /* 如果是非阻塞的,那么返回EAGAIN */
- err = -EAGAIN;
- if (!timeo)
- goto failure;
- /* 如果此时有信号,那么跳到相应处理,如:返回EINTR等 */
- if (signal_pending(current))
- goto interrupted;
- /* 这里是进行pendding, 等待其他线程释放对应的写内存 */
- timeo = sock_wait_for_wmem(sk, timeo);
- }
- /* 设定sk_buff的owner为sk,成功返回 */
- skb_set_owner_w(skb, sk);
- return skb;
-
- interrupted:
- err = sock_intr_errno(timeo);
- failure:
- *errcode = err;
- return NULL;
- }
从上面的示意代码中能够看出,系统在每次发送数据包前会检测已经发送的数据量,如果已经申请还未发送的数据超过了设定的发送缓冲区大小,那么就会阻塞住发送线程,或者返回EAGAIN。
同样在接收包时通过sock_rmalloc申请sk_buff,或者通过sock_queue_rcv_skb
将sk_buff接入到指定的相应socket接受队列时,都需要检测sk_rcvbuf的大小与sk_rmem_alloc之间的关系,如果没有足够可用的rcvbuf那么则选择将收到的包丢弃,以防止过多的数据包停留在内核中,耗空系统资源。
除此外通过getsockopt与setsockopt能够获得和改变相应的接收缓冲区大小以及发送缓冲区大小。附上实验程序小代码,代码如下:
- #include <stdio.h>
- #include <errno.h>
- #include <sys/types.h>
- #include <sys/socket.h>
-
- int main()
- {
- int sock_fd = -1;
- int snd_buf_size = 0;
- socklen_t opt_size = sizeof(snd_buf_size);
- int ret = 0;
-
- sock_fd = socket(AF_INET, SOCK_STREAM, 0);
- if (sock_fd < 0)
- {
- perror("socket fail");
- goto out;
- }
- /* 获得sndbuf的长度 */
- ret = getsockopt(sock_fd, SOL_SOCKET, SO_SNDBUF, &snd_buf_size, &opt_size);
- if (ret < 0)
- {
- perror("getsockopt fail");
- printf("%d\n", errno);
- goto out;
- }
- printf("socket %d's sndbuf is %d bytes\n", sock_fd, snd_buf_size);
-
- /* 修改sndbuf的长度 */
- snd_buf_size = 10000;
- ret = setsockopt(sock_fd, SOL_SOCKET, SO_SNDBUF, &snd_buf_size, opt_size);
- if (ret < 0)
- {
- perror("getsockopt fail");
- printf("%d\n", errno);
- goto out;
- }
-
- /* 再获得sndbuf的长度 */
- ret = getsockopt(sock_fd, SOL_SOCKET, SO_SNDBUF, &snd_buf_size, &opt_size);
- if (ret < 0)
- {
- perror("getsockopt fail");
- printf("%d\n", errno);
- goto out;
- }
- printf("socket %d's sndbuf is %d bytes\n", sock_fd, snd_buf_size);
- out:
- if (sock_fd >= 0)
- {
- close(sock_fd);
- sock_fd = 0;
- }
- return 0;
- }
运行程序结果如下
- socket 3's sndbuf is 16384 bytes
- socket 3's sndbuf is 20000 bytes
上述程序很简单,有一点需要说明的是,在程序中设置的是10000,但是在内核中却乘以了2,这个至于为什么我也没有追究过,以后有机会在追踪吧。
总的来说,内核中维护缓冲区大小就是通过4个整数之间的关系搞定,在申请之前检测相应的值,如果能阻塞的阻塞,不能阻塞的放弃。由于收包都是硬件搞定,不能阻塞,也不能让其重试,所以只能选择丢弃。发包的话,可以根据write是否为阻塞,来进行相应策略。