百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

太厉害了,居然有阿里大神带我从linux5.9看网络层的设计

liebian365 2024-11-09 13:43 20 浏览 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,这样就可以灵活地拓展。但是总的来说,网络层的实现是非常复杂的,尤其到了新版的内核,本文做了个大致的介绍,后续有时间继续深入分析。

相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: