之前在前面的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是否为阻塞,来进行相应策略。
阅读(15701) | 评论(2) | 转发(3) |