太厉害了,居然有阿里大神带我从linux5.9看网络层的设计
liebian365 2024-11-09 13:43 8 浏览 0 评论
原文链接:https://mp.weixin.qq.com/s/QvHP2dsUdKEVmW12QqzpDw
原作者:编程杂技
前言:很久没有看内核的代码了,假期开始看一下,之前看了一下0.11和1.2.13的代码,虽然大致了解了一些原理,但是毕竟比较旧了,再者很多功能还没有实现,比如epoll,所以这次选取的是5.9的版本,再也不怕过时了,当然,现在内核的代码量级非常大,不可能看得完也不可能都看,只是选取自己感兴趣的一些点看一下。看内核代码,总的来说是非常有趣的,不仅是因为知其然知其所以然,而且看到朴素的c语言,还有世界级大佬写代码的思路、思想,设置注释,都是非常有意思的事情。
今天分析的内容是从socket函数开始,看看linux网络层的设计。下面我们看一下我们平时写网络编程代码时的用法。
#include <sys/socket.h>int fd = socket(...);bind(fd, ...);lisnten(fd);
我们看到网络编程中的一系列函数都是来自sys/socket.h这个头文件。这个是glibc提供的,glibc通过系统调用的方式使用操作系统提供的API。
网络层API的调用
在网络层设计中,内核并没有给每一个网络函数都提供一个系统调用,而是提供了一个统一的入口socketcall,也就是说,我们使用的网络函数,透过glibc,最后到达socketcall入口,然后进行分发,我们来看一下代码。
// call就是调用方使用的函数SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args){ unsigned long a[AUDITSC_ARGS];
unsigned long a0, a1;
int err;
unsigned int len;
call = array_index_nospec(call, SYS_SENDMMSG + 1);
len = nargs[call];
if (copy_from_user(a, args, len))
return -EFAULT;
err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
if (err)
return err;
a0 = a[0];
a1 = a[1];
switch (call) {
case SYS_SOCKET:
err = __sys_socket(a0, a1, a[2]);
break;
case SYS_BIND:
err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
break;
// 忽略很多其他case
default:
err = -EINVAL;
break;
}
return err;}
我们看到socketcall的逻辑就是分发。下面我们以socket函数为例,继续分析网络层的设计。socket是linux网络编程中最重要的概念,socket又叫套接字,它是内核设计者对底层协议的抽象,然后提供给用户的入口,它类似工厂模式,当我们调用socket函数的时候,传入对应的参数,就可以得到不同类型的socket,对调用方来说简化了网络编程的难度,而在抽象的底层,socket包含了非常多复杂的逻辑,比如TCP、UDP、IP协议的实现。我们来看看面对复杂的网络协议,内核设计者是如何设计网络层的架构的。
网络层和文件系统的关系
我们知道Linux万物皆文件,socket也不例外,当调用socket函数的时候,我们拿到的不是socket本身,而是一个文件描述符fd。那么网络层是如何和文件系统关联起来的呢?这得益于Linux的VFS(虚拟文件系统),VFS为文件系统抽象了一套API,实现了该系列API就可以把对应的资源当作文件使用,我们来看看网络层中关于这部分的实现。我们知道文件系统有以下关系。
我们来看看inode和socket是如何关联起来的。以下是网络层中,关于超级块接口的实现
static const struct super_operations sockfs_ops = {
.alloc_inode = sock_alloc_inode,
.free_inode = sock_free_inode,
.statfs = simple_statfs,};
当调用socket函数的时候就需要新建一个inode,然后就会调用sock_alloc_inode。
struct socket_alloc {
struct socket socket;
struct inode vfs_inode;};
static struct inode *sock_alloc_inode(struct super_block *sb){ struct socket_alloc *ei;
ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
// 忽略初始化字段的逻辑
return &ei->vfs_inode;}
我们看到sock_alloc_inode函数的逻辑非常简单,就是分配一个socket_alloc结构体,socket_alloc结构体中有两个字段,分别是socket和inode,inode是给文件系统使用的,socket是网络层使用的。所以有以下关系。
网络层的初始化
从socket函数的定义中我们看到有family和type两个参数,这两个属性都会对应不同的实现。我们先看看family的实现。
static const struct net_proto_family __rcu *net_families[NPROTO] __read_mostly;
net_families是个结构体数组,那么值是什么呢?net_families的值是通过sock_register设置的。
int sock_register(const struct net_proto_family *ops){
int err;
spin_lock(&net_family_lock);
// 把ops加入到数组中
rcu_assign_pointer(net_families[ops->family], ops);
spin_unlock(&net_family_lock);
return err;}
那么哪里会调用sock_register呢?在网络层初始化的时候会调用inet_init,在inet_init中会调用sock_register。
static const struct net_proto_family inet_family_ops = {
.family = PF_INET,
.create = inet_create,
.owner = THIS_MODULE,};
static int __init inet_init(void) { (void)sock_register(&inet_family_ops);}
接着我们看看type的实现。
static struct inet_protosw inetsw_array[] ={
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
// 给sock结构体使用
.prot = &tcp_prot,
// 给socket结构体使用
.ops = &inet_stream_ops,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.flags = INET_PROTOSW_PERMANENT,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_ICMP,
.prot = &ping_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
},
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP, /* wild card */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.flags = INET_PROTOSW_REUSE,
}};
在网络层初始化的时候会注册以上的定义。
for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
inet_register_protosw(q);
我们看一下inet_register_protosw
void inet_register_protosw(struct inet_protosw *p){
struct list_head *lh;
struct inet_protosw *answer;
int protocol = p->protocol;
struct list_head *last_perm;
spin_lock_bh(&inetsw_lock);
last_perm = &inetsw[p->type];
list_for_each(lh, &inetsw[p->type]) {
answer = list_entry(lh, struct inet_protosw, list);
if ((INET_PROTOSW_PERMANENT & answer->flags) == 0)
break;
if (protocol == answer->protocol)
goto out_permanent;
last_perm = lh;
}
list_add_rcu(&p->list, last_perm);}
inet_register_protosw主要是把inet_protosw结构体逐个加入到队列中,后面会用到。接下来我们就可以分析socket函数的实现了。
socket函数的实现
socket函数对应的实现是__sys_socket。
int __sys_socket(int family, int type, int protocol){
int retval;
struct socket *sock;
int flags;
/*
type这个字段中一部分数据是标记协议的类型的,比如流式类型、数据包类型,
另一部分数据是标记socket的特性的,比如非阻塞SOCK_NONBLOCK,这个是新
内核支持的,以前需要使用两个函数完成
*/
flags = type & ~SOCK_TYPE_MASK;
if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
return -EINVAL;
type &= SOCK_TYPE_MASK;
if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
//
retval = sock_create(family, type, protocol, &sock);
if (retval < 0)
return retval;
// 获取一个fd
return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));}
socket函数实现中,主要分为两个部分,分别是sock_create和sock_map_fd。sock_create顾名思义是用于创建一个socket结构体的。
int sock_create(int family, int type, int protocol, struct socket **res){
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);}
继续看__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;
// 申请一个socket结构体
sock = sock_alloc();
sock->type = type;
rcu_read_lock();
// 根据socket类型(协议簇类型)拿到对应的处理函数集
pf = rcu_dereference(net_families[family]);
pf->create(net, sock, protocol, kern);
*res = sock;
return 0;}
__sock_create函数调用sock_alloc分配一个结构体
struct socket *sock_alloc(void){
struct inode *inode;
struct socket *sock;
// 申请一个inode
inode = new_inode_pseudo(sock_mnt->mnt_sb);
// inode和socket在同一个结构体中,取出来
sock = SOCKET_I(inode);
// 设置inode号和属性
inode->i_ino = get_next_ino();
inode->i_mode = S_IFSOCK | S_IRWXUGO;
inode->i_uid = current_fsuid();
inode->i_gid = current_fsgid();
inode->i_op = &sockfs_inode_ops;
return sock;}
struct inode *new_inode_pseudo(struct super_block *sb){ struct inode *inode = alloc_inode(sb);
if (inode) {
spin_lock(&inode->i_lock);
inode->i_state = 0;
spin_unlock(&inode->i_lock);
INIT_LIST_HEAD(&inode->i_sb_list);
}
return inode;}
static struct inode *alloc_inode(struct super_block *sb){ const struct super_operations *ops = sb->s_op;
struct inode *inode;
// alloc_inode指向sock_alloc_inode函数
if (ops->alloc_inode)
inode = ops->alloc_inode(sb);
return inode;}
static struct inode *sock_alloc_inode(struct super_block *sb){ struct socket_alloc *ei;
ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
return &ei->vfs_inode;}
sock_alloc申请了一个socket_alloc结构体,然后返回socket_alloc结构体中的socket结构体。接着我们看看又做了什么操作。
// 根据socket类型拿到对应的处理方式
pf = rcu_dereference(net_families[family]);
pf->create(net, sock, protocol, kern);
前面我们已经分析过net_families,这里会调family对应的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;
sock->state = SS_UNCONNECTED;
/* Look for the requested type/protocol pair. */
lookup_protocol:
err = -ESOCKTNOSUPPORT;
rcu_read_lock();
// 遍历找到对应类型的处理方式
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
err = 0;
/* Check the non-wild match. */
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;
sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
// 关联socket和sock结构体
sock_init_data(sock, sk);
if (sk->sk_prot->init) {
err = sk->sk_prot->init(sk);
}}struct sock *sk_alloc(struct net *net, int family, gfp_t priority,
struct proto *prot, int kern){
struct sock *sk;
// 创建一个sock结构体
sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family);
if (sk) {
sk->sk_family = family;
// 赋值函数集
sk->sk_prot = sk->sk_prot_creator = prot;
}
return sk;}
inet_create函数主要的逻辑主要是根据type找到对应协议的处理函数集,然后把对应的函数集赋值给socket结构体(后面会用到),接着创建一个sock结构体并把对应的函数集合赋值给sock结构体。接着把sock和socket结构体关联起来。最后调用sock的init函数。我们以UDP为例。
int udp_init_sock(struct sock *sk){
skb_queue_head_init(&udp_sk(sk)->reader_queue);
sk->sk_destruct = udp_destruct_sock;
return 0;}
只是做了一些初始化的处理。最后形成以下架构。
很多同学应该都知道Linux万物皆文件的哲学思想,当我们调用socket拿到一个结构体后,并不是把这个结构体返回给调用方,而是返回一个文件描述符fd。这个fd就像一个索引一样,后续就可以通过该fd找到对应的socket结构体。我们看看是怎么处理的。
static int sock_map_fd(struct socket *sock, int flags){
struct file *newfile;
// 获取一个可用的fd
int fd = get_unused_fd_flags(flags);
// 获取一个可用的file结构体
newfile = sock_alloc_file(sock, flags, NULL);
// 关联fd和file结构体
if (!IS_ERR(newfile)) {
fd_install(fd, newfile);
return fd;
}
put_unused_fd(fd);
return PTR_ERR(newfile);}
至此网络层的整体架构就分析完了,我们再看一下这种架构的好处是什么,比如我们后续会调用bind函数。
int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen){
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
// 根据fd找到对应的socket结构体
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
err = move_addr_to_kernel(umyaddr, addrlen, &address);
if (!err) {
err = security_socket_bind(sock,
(struct sockaddr *)&address,
addrlen);
if (!err)
// 调用函数集的bind函数
err = sock->ops->bind(sock,
(struct sockaddr *)
&address, addrlen);
}
fput_light(sock->file, fput_needed);
}
return err;}
我能看到后续的调用的网络函数中,会调用底层对应的函数,当我们新增一个协议的时候,只需要实现对应的API就可以。我们看到整个网络层的实际中,主要分为socket层、af_inet层和具体协议层(TCP、UDP等)。当我使用网络编程的时候,首先会创建一个socket结构体(socket层),socket结构体是最上层的抽象,然后通过协议簇类型创建一个对应的sock结构体,sock是协议簇抽象(af_inet层),同一个协议簇下又分为不同的协议类型,比如TCP、UDP(具体协议层),然后根据socket的类型(流式、数据包)找到对应的操作函数集并赋值到socket和sock结构体中,后续的操作就调用对应的函数就行,调用某个网络函数的时候,会从socket层到af_inet层,af_inet做了一些封装,必要的时候调用底层协议(TCP、UDP)对应的函数。而不同的协议只需要实现自己的逻辑就能加入到网络协议中。
总结,网络层的设计VFS有点相似,通过在上层定义抽象的API,底层实现具体的API,这样就可以灵活地拓展。但是总的来说,网络层的实现是非常复杂的,尤其到了新版的内核,本文做了个大致的介绍,后续有时间继续深入分析。
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)