本文将对DragonOS网络子系统进行简要介绍。出于“快速实现功能”的考虑,DragonOS目前网络子系统基于Smoltcp协议栈进行开发,具体协议部分采用smoltcp的实现。计划在将来重构网络子系统时,采用独立开发的协议栈,以支持“服务器系统”的需求。
分层设计
网络子系统自上而下分为:系统调用接口层、Socket层、Smoltcp协议栈、网络设备层。目前底层承接收发网络功能的是VirtIO网卡。
系统调用接口层
系统调用接口层对外提供了十多个符合posix规范的接口,语义与Linux一致。在这一层内,会对用户传入的参数进行基本的校验,并把Posix规定的参数类型,转换为DragonOS具体实现的类型。然后,调用Socket层的函数,进行操作。
以下是当前提供的所有系统调用接口:
- sys_accept
- sys_bind
- sys_connect
- sys_getpeername
- sys_getsockname
- sys_getsockopt
- sys_listen
- sys_recvfrom
- sys_recvmsg
- sys_sendto
- sys_setsockopt
- sys_shutdown
- sys_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。
代码语言:javascript复制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中传递的。
代码语言:javascript复制void tcp_server()
{
printf("TCP Server is running...n");
server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("socket() ok, server_sockfd=%dn", 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() okn");
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=%dn", 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=%dn", len);
break;
}
if (strcmp(buffer, "exitn") == 0)
{
break;
}
printf("Received: %sn", buffer);
send(conn, buffer, len, 0);
}
close(conn);
close(server_sockfd);
}
创建socket
代码语言:javascript复制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的文件描述符了。
绑定端口
代码语言:javascript复制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,记录端口分配情况。
监听端口
代码语言:javascript复制 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已经在监听。
接受连接
代码语言:javascript复制 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,可以用它来发送数据。
代码语言:javascript复制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
接收数据
代码语言:javascript复制int len = recv(conn, buffer, sizeof(buffer), 0);
这行代码会发起一个sys_recvfrom系统调用,然后进入到这些地方:
- https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/syscall.rs?r=cde5492f&mo=14597&fi=491#324
- https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/syscall.rs?r=cde5492f&mo=14597&fi=491#353
- https://opengrok.ringotek.cn/xref/DragonOS/kernel/src/net/socket.rs?r=cde5492f#566
由于设计思路与前面的类似,在这里不多赘述。主要就是对连接状态进行判断、对端点地址进行处理,然后发送数据。
以上就是本文的全部内容,转载请注明来源:https://longjin666.cn/?p=1729