本文将对DragonOS网络子系统进行简要介绍。出于“快速实现功能”的考虑,DragonOS目前网络子系统基于Smoltcp协议栈进行开发,具体协议部分采用smoltcp的实现。计划在将来重构网络子系统时,采用独立开发的协议栈,以支持“服务器系统”的需求。

分层设计

网络子系统自上而下分为:系统调用接口层、Socket层、Smoltcp协议栈、网络设备层。目前底层承接收发网络功能的是VirtIO网卡。

系统调用接口层

系统调用接口层对外提供了十多个符合posix规范的接口,语义与Linux一致。在这一层内,会对用户传入的参数进行基本的校验,并把Posix规定的参数类型,转换为DragonOS具体实现的类型。然后,调用Socket层的函数,进行操作。

以下是当前提供的所有系统调用接口:

具体的接口含义请看函数注释

Socket层

Socket层是对smoltcp协议栈的各种socket的封装。原因在于,smoltcp提供的socket,只是非常“纯粹”的功能,不包含操作系统所需的诸如端口绑定管理(bind的时候需要找空闲端口)之类的功能。并且,Socket层还将每个Socket抽象为一个Inode,使得用户能像读写文件一样操作socket。

目前DragonOS仅支持TCP、UDP、Raw Socket,可考虑添加NetLink Socket和Packet Socket的支持。这两个功能的实现,也是基于smoltcp协议栈的基础能力,在Socket层封装新的Socket类型即可实现。

Smoltcp协议栈

smoltcp是一个独立的、事件驱动的TCP/IP堆栈,专为裸机实时系统设计。它的设计目标是简单和可靠。

之所以选择smoltcp协议栈,很重要的原因在于,它是用Rust编写的,能够较好的与现有代码融合,并提供较好的安全性和可靠性。

它目前作为DragonOS网络子系统的一部分,支撑了网络子系统的主要功能。但是,在开发过程中,我们发现它存在一定的问题:由于其设计为嵌入式的网络栈,并且它为了在嵌入式场景实现的简便性,对多请求并发的场景表现的并不是特别好。举例:

  • smoltcp不支持backlog,因此在多请求同时到达的情况下,容易出现connection refused的情况。
  • 协议栈内过早地获取设备的可变引用,导致临界区较大,导致显而易见的性能损耗。

网络设备层

目前把所有的网络设备都放置在了一个叫做NET_DRIVERS的BTreeMap之中。由于网卡中断尚未实现,因此目前会通过系统定时器机制,每10ms轮询所有网卡,读取数据并更新socket的状态。

所有网络设备都需要实现一个叫做“NetDriver”的trait。

pub trait NetDriver: Driver {
    /// @brief 获取网卡的MAC地址
    fn mac(&self) -> EthernetAddress;

    fn name(&self) -> String;

    /// @brief 获取网卡的id
    fn nic_id(&self) -> usize;

    fn poll(&self, sockets: &mut iface::SocketSet) -> Result<(), SystemError>;

    fn update_ip_addrs(&self, ip_addrs: &[wire::IpCidr]) -> Result<(), SystemError>;

    /// @brief 获取smoltcp的网卡接口类型
    fn inner_iface(&self) -> &SpinLock<smoltcp::iface::Interface>;
}

一个TCP数据包的发送过程

由于DragonOS的网络相关系统调用语义与Linux一致,因此在Linux上能正常运行的tcp服务器程序,也能正常在DragonOS上运行。

下面对这样一段代码片段所实现的tcp服务器功能进行追踪,帮助我们了解TCP数据包是如何在DragonOS中传递的。

void tcp_server()
{
    printf("TCP Server is running...\n");
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    printf("socket() ok, server_sockfd=%d\n", server_sockfd);
    struct sockaddr_in server_sockaddr;
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_port = htons(SERVER_PORT);
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(server_sockfd, (struct sockaddr *)&server_sockaddr, sizeof(server_sockaddr)))
    {
        perror("Server bind error.\n");
        exit(1);
    }

    printf("TCP Server is listening...\n");
    if (listen(server_sockfd, CONN_QUEUE_SIZE) == -1)
    {
        perror("Server listen error.\n");
        exit(1);
    }

    printf("listen() ok\n");

    char buffer[BUFFER_SIZE];
    struct sockaddr_in client_addr;
    socklen_t client_length = sizeof(client_addr);
    /*
        Await a connection on socket FD.
        When a connection arrives, open a new socket to communicate with it,
        set *ADDR (which is *ADDR_LEN bytes long) to the address of the connecting
        peer and *ADDR_LEN to the address's actual length, and return the
        new socket's descriptor, or -1 for errors.
     */
    conn = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_length);
    printf("Connection established.\n");
    if (conn < 0)
    {
        printf("Create connection failed, code=%d\n", conn);
        exit(1);
    }
    send(conn, logo, sizeof(logo), 0);
    while (1)
    {
        memset(buffer, 0, sizeof(buffer));
        int len = recv(conn, buffer, sizeof(buffer), 0);
        if (len <= 0)
        {
            printf("Receive data failed! len=%d\n", len);
            break;
        }
        if (strcmp(buffer, "exit\n") == 0)
        {
            break;
        }

        printf("Received: %s\n", buffer);
        send(conn, buffer, len, 0);
    }
    close(conn);
    close(server_sockfd);
}

创建socket

server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

这样代码会发起sys_socket系统调用,进入到这里:

https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/syscall.rs?r=cde5492f&mo=588&fi=24#24

sys_socket函数会抽出参数,然后传入到do_socket函数内。

do_socket函数在这里:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/syscall.rs?r=cde5492f&mo=588&fi=24#39

在do_socket函数内,将会根据地址族和socket类型创建不同类型的socket。然后把创建好的SocketInode存入进程的文件描述符数组中。

在do_socket函数内,会调用Tcp Socket的new方法,创建tcp socket。

TcpSocket::new()

代码:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/socket.rs?r=cde5492f#522

在Tcp Socket的new方法内,会进行以下操作:

  • 为新的socket创建发送、接收缓冲区
  • 在smoltcp协议栈中,创建socket
  • 把Socket添加到全局的SOCKET_SET集合之中,并且获得一个SocketHandle.
  • 创建TcpSocket结构体,存放上述的SocketHandle.
  • 返回创建好的TcpSocket结构体。

然后逐级返回,就回到了用户态,用户程序就能获取到这个新的socket的文件描述符了。

绑定端口

if (bind(server_sockfd, (struct sockaddr *)&server_sockaddr, sizeof(server_sockaddr)))

用户程序的这行代码中的bind()函数,会发起一个sys_bind()系统调用,进入到内核的这里:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/syscall.rs?r=cde5492f#242

提取出参数后,进入do_bind函数:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/syscall.rs?r=cde5492f#258

这个函数会根据用户传入的参数,生成一个endpoint结构体。并且,调用socket的bind方法,将socket与端口绑定,然后返回。

由于当前socket是Tcp Socket,因此264行实际调用的是Tcp的bind方法:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/socket.rs?r=cde5492f#737

这个方法里面,当传入的端口号为0时,内核会自动分配一个未使用的端口,作为本地的端口。请注意,这里的第264行的get_ephemeral_port方法并不能保证端口一定未使用。这是接下来需要优化的地方:为内核维护一个ListenTable,记录端口分配情况。

监听端口

    if (listen(server_sockfd, CONN_QUEUE_SIZE) == -1)

这行代码会发起一个sys_listen系统调用,以实现对端口的监听。请注意,由于smoltcp不支持backlog,因此listen()函数的第二个参数值无效,内核的动作永远与backlog=1时相同。

代码:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/syscall.rs?r=cde5492f&mo=13292&fi=443#443

与上面相似的,这里在do_listen里面,调用socket的listen方法,完成监听过程。

这里由于是TcpSocket,因此调用的是TcpSocket的listen方法:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/socket.rs?r=cde5492f#720

内部则是调用smoltcp的listen方法,表示当前socket已经在监听。

接受连接

    conn = accept(server_sockfd, (struct sockaddr *)&client_addr, &client_length);

这行用户代码会接受一个来自外部的tcp连接,并且返回新的连接的socket fd.

这里会发起一个叫做sys_accept的系统调用:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/syscall.rs?r=cde5492f&mo=14597&fi=491#491

这个系统调用与上面的类似,也是取出参数,然后调用内层socket的accept方法(这里就是TcpSocket的accept方法)。最后把新创建的文件描述符写入到进程的文件描述符数组内,然后返回。

与上面的不同,这里还多了一个“向用户空间写入客户端地址”的过程,也就是写入上述的”client_addr”和”client_length”这两个变量。

TcpSocket::accept()

这个函数在这里:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/socket.rs?r=cde5492f#757

它其实是一个不断轮询的过程,每个循环前,都轮询一遍所有的网卡,更新socket状态。接着再获取当前socket,判断是否为active。如果连接已经启动,那么就尝试获取远端地址,并创建新的socket来存储。如果连接没启动,就将当前进程加入等待队列,待全局SOCKET_SET中有socket的状态更新后,再检查当前socket是否已经连接。

注:是的,这里很低效,因此需要想个办法优化它。但是smoltcp在poll_iface的时候,并没有告诉我们到底是哪些sockets被更新了,它只是告诉我们有socket的状态被更新了。因此暂时没想到更好的方法。

这里需要注意的是,返回给用户的是一个新的Socket对象。而当前服务端监听用的socket已经与对端建立连接,因此,需要这样做:把监听用的socket的handle,转移给新创建的socket,接着为服务端监听用的socket创建新的handle。这样下来,监听用的socket才能再次与新的客户端建立连接。

发送数据

当连接被建立后,我们得到了一个新的socket,可以用它来发送数据。

send(conn, logo, sizeof(logo), 0);

发送数据的方法有几种,比如可以通过文件的write方法来发送,也可以使用网络的send方法。

这里会产生一个sys_sendto系统调用:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/syscall.rs?r=cde5492f&mo=14597&fi=491#298

最终调用的是tcp socket的write方法:

TcpSocket::write()

tcp socket的write方法较为简单,判断了连接是否打开,以及是否能够发送。接着就发送数据了:https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/socket.rs?r=cde5492f#617

接收数据

int len = recv(conn, buffer, sizeof(buffer), 0);

这行代码会发起一个sys_recvfrom系统调用,然后进入到这些地方:

由于设计思路与前面的类似,在这里不多赘述。主要就是对连接状态进行判断、对端点地址进行处理,然后发送数据。

以上就是本文的全部内容,转载请注明来源:https://longjin666.cn/?p=1729

欢迎关注我的公众号“灯珑”,让我们一起了解更多的事物~

你也可能喜欢

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注