记一次Linux内核中socket源码走读
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了记一次Linux内核中socket源码走读,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含6870字,纯文字阅读大概需要10分钟。
内容图文
在熟悉TCP协议的原理后,我们知道TCP由于维护可靠性连接,其中的过程和算法是很复杂的。但是在实际开发中,一般只需要调用api提供的几个函数即可。更有甚者,现在各种框架将网络层包起来了,只留下应用层的读写调用,无疑大大降低了开发成本。 但是,我们带着疑问“究竟在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 层。分别对应于应用层和内核层的操作。
内容总结
以上是互联网集市为您收集整理的记一次Linux内核中socket源码走读全部内容,希望文章能够帮你解决记一次Linux内核中socket源码走读所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。