记一次Linux内核中socket源码走读
作者:互联网
但是,我们带着疑问“究竟在Linux下是如何实现socket的?”
1、原理与使用
一般而言,使用socket的接口创建一个socket,用如下构造函数。
int socket(int domain, int type, int protocol)
domain 就是指 PF_INET、PF_INET6 以及 PF_LOCAL 等,表示IPV4,IPV6或者域套接字等套接字类型。
type 可用的值是:SOCK_STREAM: 表示的是字节流,对应 TCP;SOCK_DGRAM:表示的是数据报,对应 UDP;SOCK_RAW: 表示的是原始套接字。
下面我们看一个建立的服务端创建的例子,首先使用socket接口创建一个socket,然后调用bind函数绑定本地端口。
int make_socket (uint16_t port){ int sock; struct sockaddr_in name; /* 创建字节流类型的IPV4 socket. */ sock = socket (PF_INET, SOCK_STREAM, 0); if (sock < 0) { perror ("socket"); exit (EXIT_FAILURE); } /* 绑定到port和ip. */ name.sin_family = AF_INET; /* IPV4 */ name.sin_port = htons (port); /* 指定端口 */ name.sin_addr.s_addr = htonl (INADDR_ANY); /* 通配地址 */ /* 把IPV4地址转换成通用地址格式,同时传递长度 */ if (bind(sock, (struct sockaddr *) &name, sizeof (name))< 0) { perror ("bind"); exit (EXIT_FAILURE); } return sock;}//然后服务端需要listen端口,accept连接
2、Linux源码走读
Linux源码的socket是从系统调用开始,如下所示:
SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol){ int retval; struct socket *sock; int flags; ...... if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; retval = sock_create(family, type, protocol, &sock);//① ...... retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); ...... return retval; }其中,sock_create函数是创建一个socket,然后调用sock_map_fd是为了与文件描述符绑定,因为在Linux下一切皆文件。 为了方便阅读下面的源码,我绘制了一个流程图,照着这个流程图的思路往下阅读。
我们看①处的sock_create函数,调用了下面的__sock_create函数。
int __sock_create(struct net *net, int family, int type, int protocol,struct socket **res, int kern){ int err; struct socket *sock; const struct net_proto_family *pf; ...... sock = sock_alloc(); ...... sock->type = type; ...... pf = rcu_dereference(net_families[family]); ...... err = pf->create(net, sock, protocol, kern); ...... *res = sock; return 0;}这里主要是调用sock_alloc函数分配了一个struct socket结构。然后调用rcu_dereference函数,看看这个函数干嘛的。其中的参数net_families的结构如下:
static const struct net_proto_family inet_family_ops = { .family = PF_INET, .create = inet_create,//这个用于socket系统调用创建 ......}到这里,也就是说net_families数组是一个协议簇的数组,每个元素对应一个协议,比如IPV4协议簇,IPV6协议簇。我们上面举例的socket中的参数传递的是PF_INET,一直到这里的net_proto_family结构。其实,也就是根据socket一路找到了应该调用的回调。因此,pf->create函数就是调用的net_proto_family中的inet_create回调函数。
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern){ struct sock *sk; struct inet_protosw *answer; struct inet_sock *inet; struct proto *answer_prot; unsigned char answer_flags; int try_loading_module = 0; int err; /* Look for the requested type/protocol pair. */ lookup_protocol: list_for_each_entry_rcu(answer, &inetsw[sock->type], list) { err = 0; /* Check the non-wild match. yishuihan*/ if (protocol == answer->protocol) { if (protocol != IPPROTO_IP) break; } else { /* Check for the two wild cases. */ if (IPPROTO_IP == protocol) { protocol = answer->protocol; break; } if (IPPROTO_IP == answer->protocol) break; } err = -EPROTONOSUPPORT; }...... sock->ops = answer->ops; answer_prot = answer->prot; answer_flags = answer->flags;...... sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);......}list_for_each_entry_rcu函数用于循环查看inetsw[sock->type],也就是inetsw数组是一个协议的数组,比如tcp协议,UDP协议都对应一个协议元素。因此,这里是找到SOCK_STREAM参数对应的数组元素。最终调用到inet_init函数。
static int __init inet_init(void){ /* Register the socket-side information for inet_create. */ for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r) INIT_LIST_HEAD(r); for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q) inet_register_protosw(q); //省略其他代码... yishuihan}这里的第一个for循环将inetsw数组组成了一个链表,因为协议的类型比较多,比如TCP,UDP,等等。第二个循环是将inetsw_array数组注册到inetsw数组里面。inetsw_array定义的结构如下所示。比如创建了很多tcp的socket,这里可以看成每个socket都要被关联到tcp协议这个inetsw元素下面。
static struct inet_protosw inetsw_array[] ={ { .type = SOCK_STREAM, .protocol = IPPROTO_TCP, .prot = &tcp_prot, .ops = &inet_stream_ops, .flags = INET_PROTOSW_PERMANENT| INET_PROTOSW_ICSK, }, //省略其他协议,比如UDP等.... yishuihan}
从inet_create 的 list_for_each_entry_rcu 循环中开始,这是在 inetsw 数组中,根据 type 找到属于这个类型的列表,然后依次比较列表中的 struct inet_protosw 的 protocol 是不是用户指定的 protocol;如果是,就得到了符合用户指定的 family->type->protocol 的 struct inet_protosw 类型的*answer 对象。
接下来,struct socket *sock 的 ops 成员变量,被赋值为 answer 的 ops。对于 TCP 来讲,就是 inet_stream_ops。后面任何用户对于这个 socket 的操作,都是通过 inet_stream_ops 进行的。接下来,我们创建一个 struct sock *sk 对象。
socket 和 sock 看起来几乎一样,实际上socket 是用于负责对上给用户提供接口,并且和文件系统已经关联。而sock则负责向下对接内核网络协议栈。在sk_alloc 函数中,struct inet_protosw *answer 结构的 tcp_prot 赋值给了 struct sock *sk 的 sk_prot 成员。tcp_prot 的定义如下,里面定义了很多的函数,都是 sock 之下内核协议栈的动作。tcp_prot 的回调函数如下,就是我们比较熟悉的tcp协议内容了。
struct proto tcp_prot = { .name = "TCP", .owner = THIS_MODULE, .close = tcp_close, .connect = tcp_v4_connect, .disconnect = tcp_disconnect, .accept = inet_csk_accept, .ioctl = tcp_ioctl, .init = tcp_v4_init_sock, .destroy = tcp_v4_destroy_sock, .shutdown = tcp_shutdown, .setsockopt = tcp_setsockopt, .getsockopt = tcp_getsockopt, .keepalive = tcp_set_keepalive, .recvmsg = tcp_recvmsg, .sendmsg = tcp_sendmsg, .sendpage = tcp_sendpage, .backlog_rcv = tcp_v4_do_rcv, .release_cb = tcp_release_cb, .hash = inet_hash, .get_port = inet_csk_get_port, ......}
3 总结
Socket 系统调用会有三级参数 family、type、protocal,通过这三级参数,分别在 net_proto_family 表中找到 type 链表,在 type 链表中找到 protocal 对应的操作。这个操作分为两层,对于 TCP 协议来讲,第一层是 inet_stream_ops 层,第二层是 tcp_prot 层。分别对应于应用层和内核层的操作。
标签:socket,int,走读,sock,tcp,源码,inet,struct 来源: https://blog.51cto.com/15060546/2641165