Netlink 概述
Netlink是Linux内核和用户空间之间的通信机制,用于传递网络配置和管理信息。它是一个双向的、全双工的套接字接口,支持多种协议,最常用于网络子系统。
在早期的 Linux 版本中,网络配置主要通过 ioctl 系统调用完成。然而,随着网络协议栈和配置需求的复杂化,ioctl 方式显得笨重且不够灵活。Netlink 机制应运而生,提供了一种高效、灵活的内核和用户空间通信方式,特别适用于网络配置和管理。
PS:比如 RDMA链接就是通过NETLINK 创建newlink的,具体可以参考rxe_newlink.
demo code of kernel
#include <linux/kernel.h>
#include <linux/module.h>
#include <net/net_namespace.h>
#include <linux/netlink.h>
#include <net/sock.h>
#define NETLINK_TEST 20
static struct sock *netlink_test_sk;
static void netlink_test_rcv(struct sk_buff *skb)
{
struct sk_buff *skb_in, *skb_out;
struct nlmsghdr *nlh;
char message[256] = {0};
int seq, pid;
unsigned int datalength;
skb_in = skb_get(skb);
if (skb_in->len >= nlmsg_total_size(0)) {
nlh = nlmsg_hdr(skb_in);
pid = nlh->nlmsg_pid;
seq = nlh->nlmsg_seq;
printk("message received from process %d: %s\n", pid, (char*)NLMSG_DATA(nlh));
sprintf(message, "hello user space, your pid is %d", pid);
datalength = strlen(message) + 1;
skb_out = nlmsg_new(NLMSG_LENGTH(datalength), GFP_KERNEL);
nlh = nlmsg_put(skb_out, pid, seq, 0, NLMSG_ALIGN(datalength), 0);
memcpy(nlmsg_data(nlh), message, datalength);
netlink_unicast(netlink_test_sk, skb_out, pid, MSG_DONTWAIT);
}
kfree_skb(skb_in);
//dump_stack();
}
int __init netlink_test_init(void)
{
struct netlink_kernel_cfg cfg = {
.groups = 0,
.input = netlink_test_rcv,
};
netlink_test_sk = netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
if (!netlink_test_sk)
return -ENOMEM;
return 0;
}
void __exit netlink_test_exit(void)
{
netlink_kernel_release(netlink_test_sk);
}
module_init(netlink_test_init);
module_exit(netlink_test_exit);
MODULE_LICENSE("GPL");
user space program:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <linux/netlink.h>
#include <unistd.h>
#define NETLINK_TEST 20
#define MSG_LEN 100
int main(int argc, char* argv[])
{
char *data = "hello kernel space!";
struct sockaddr_nl local, dest;
int skfd, ret;
unsigned int datalength, addrlength;
struct nlmsghdr *msg_out, *msg_in;
skfd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(skfd < 0){
perror("create socket failed");
return 0;
}
memset(&local, 0, sizeof(struct sockaddr_nl));
local.nl_family = AF_NETLINK;
local.nl_pid = getpid();
local.nl_groups = 0;
if(bind(skfd, (struct sockaddr *)&local, sizeof(struct sockaddr_nl)) != 0){
perror("bind failed");
close(skfd);
return 0;
}
memset(&dest, 0, sizeof(struct sockaddr_nl));
dest.nl_family = AF_NETLINK;
dest.nl_pid = 0;
dest.nl_groups = 0;
datalength = strlen(data) + 1;
msg_out = (struct nlmsghdr*)malloc(NLMSG_SPACE(datalength));
memset(msg_out, 0, NLMSG_SPACE(datalength));
msg_out->nlmsg_len = NLMSG_SPACE(datalength);
msg_out->nlmsg_flags = 0;
msg_out->nlmsg_type = 0;
msg_out->nlmsg_seq = 0;
msg_out->nlmsg_pid = local.nl_pid;
memcpy(NLMSG_DATA(msg_out), data, strlen(data));
ret = sendto(skfd, msg_out, msg_out->nlmsg_len, 0, (struct sockaddr*)&dest, sizeof(struct sockaddr_nl));
if(!ret){
perror("send failed");
free(msg_out);
close(skfd);
return 0;
}
free(msg_out);
msg_in = (struct nlmsghdr*)malloc(NLMSG_SPACE(MSG_LEN));
memset(msg_in, 0, NLMSG_SPACE(MSG_LEN));
addrlength = sizeof(struct sockaddr_nl);
ret = recvfrom(skfd, msg_in, NLMSG_SPACE(MSG_LEN), 0, (struct sockaddr*)&dest, &addrlength);
if(!ret){
perror("recv failed");
free(msg_in);
close(skfd);
return 0;
}
printf("message received from kernel: %s\n", (char*)NLMSG_DATA(msg_in));
free(msg_in);
close(skfd);
return 0;
}
running result:

relationship between socket and file

this is very same like the relationship between dmabuf and file.


callstack of netlink coummunication


get socket from file

there are two kinds of netlink,kernel netlink and user netlink,the difference s that the kernel netlink nladdr->nl_pid is zero,but user netlink is the process`s pid(tgid).

用户态的struct socket对象和内核态的struct sock对象是一一对应的,彼此之间互相引用:
所以,通过用户态的socket fd就可以找到struct socket对象,进而找到struct sock对象,利用struct sock对象丰富的ops,即可完成数据的收发。
netlink family register

difference between kernel socket and user socket

kernel address is zero, but user space socket are the pid of spciefic Process.
1.netlink 协议下,用户socket向内核socket发送数据时,发送堆栈是直接从系统调用下来调用input 回调的,input是从kernel view角度看的,中间没有经过上下文切换。
2.netlink协议下,内核socket向用户socket发送数据时,用户内核不可能直接调用用户函数,发送堆栈经过了上下文切换。两方通过用户态sk的sk_receive_queue队列交互。
3.用户和内核通过port id查找对方的socket,用户的port id是进程PID,而内核的port id是0.调用netlink_getsockbyportid即可茶找到对方的SOCKET,这有点像TCP/IP协议中,源端口和目标端口的端口互换。
用户态socket和内核态socket转化过程:
数据传输流:
接收的时候,貌似内核的sk_receive_queue没有起到作用,是直接下来的。
net_proto_family ops
说到注册新的协议族(下文用family描述),我们需要再次描述一下数组struct net_proto_family net_families,net_families数组的每一项是一个结构体指针,指向一个描述family的结 构体, 这个数组长度不同的内核版不同,在linux-5.15.166上,共有NPROTO(46)项,
其每一项都固 定分配给一个family使用。比如,AF_INET(因特网协议)固定占用net_families[2],如果net_families[2]== NULL,则表示当前内核没有AF_INET模块,不支持因特网协议,AF_INET6则是IP version 6协议。
net namespace
-
inux系统包括默认的命名空间 : "init_net"和用户自定义的net.
-
我们通常说的namespace 一般是默认的命名空间 : "init_net", 也就是所有的"网络通信协议"+"网络设备"都是属于默认的命名空间.
-
大多数计算机通常都只需要一个网络命名空间. 即只有默认命名空间init_net(该变量实际上是全局的,并未包含在另一个命名空间中,其定义如下:

-
init_net会被链接到net_namespace_list这个双向链表上, 定义如下所示:


-
net_namespace_list就包含了所有的网络命令空间, 其以init_net为表头
view the net_device in system
code to view the net list,可以查看seqfile case #18测试用例,输出如下:

从图中可以看到,除了后面输出的LO每个DEVICE对应一个NET空间之外,前面的几个设备共享init_net空间。这些设备就包含了本机下的无线网卡,以太网卡,DOCKER网桥以及本地回环设备等等。这个和/sys/class/net/下面的输出是对应的。可以看到,class目录下的网络设备实际上都是属于init_net空间的。并且两个BDF为02:00.0和03:00.0的两个设备分别是RealTeck的以太网控制器和高通的无线网卡。也就是这台电脑有两个网卡。

ifconfig display the "init_net" network namespace device status.

network namespace 创建
use sys_unshare syscall to create namespace.


内核网络协议栈关键数据结构组织
1.执行流为socket file ->private->struct socket->struct net_proto_family->struct proto_ops->struct proto tcp_prot.
用户态socket和内核态socket转化过程:
每个struct sk_buff对象都有->dev字段指向一个struct net_device对象,表示此数据将要通过此网卡设备对象发送/接收。
为何有了struct socket,还要创建struct sock
前面看到了,调用层次中在不同的阶段,SOCKET有两种不同的实体,这两种实体在SOCKET软件堆栈中的不同阶段,不同层次发挥作用,struct sock是AF层的socket, socket是抽象的struct sock.
socket创建平面和数据传输平面
netlink socket family struct net_proto_family netlink_family_ops是静态注册的,OPS中提供了CREATE回调创建struct sock对象并创建数据结构,这个是创建平面,在执行创建平面之前,由于没有FAMILY下层的struct sock,无法完成数据收发。
创建平面会填充struct socket 的struct proto_ops 和struct sock的 struct proto, 当创建平面完成后,就可以调用填充号的OPS回调完成数据收发了。
查看内核支持那些网络协议
sudo cat /proc/net/protocols:
本质上是内核注册支持的struct proto结构体对象:
查看内核netlink信息
$sudo cat /proc/net/netlink
信息左列是所有创建的NET LIINK SK地址,包含用户态的和内核态的,第二列是NET_LINK的类型,当前支持32种类型。
每种类型都要通过netlink_kernel_create API创建对应netlink类型的内核struct sock对象,并且这种对象的PID只能是0,第三列是PID, 根据对/proc/net/netlink节点grep到的信息,其每种TYPE的内核struct sock对象只有一个,通过netlink_kernel_create创建,并且PID为0, 和GREP到的相吻合,除此之外的所有struct sock对象都是用户态对象。
注意最后一个TYPE类型为24的NETLINK是我们创建的测试NETLINK:
可以看到运行用户态测试是,TYPE为24的netlink sock有两个,分别是内核PID为0的SOCK,以及用户态创建的PID为进程PID的SOCK,SOCK PID为21952和进程PID吻合。
netlink_bind到底bind了什么?
用户态socket bind操作是以socket fd和struct sockaddr为参数,调用bind函数的过程,socket FD代表struct socket对象,自不必说,重点是struct sockaddr对象,它包含了nl_pid信息,nl_pid相当于TCP/IP网络协议中的端口号,所以本质上是将SOCKET和端口号进行绑定:
netlink_bind:
函数核心是调用了netlink_insert或者netlink_autobind,当用户台设置了PID时,调用前者,当用户态没有设置PID时,调用后者,后者实际上也是调用前者,只是在调用前会首先调用task_tgid_vnr(current)获取当前任务的PID:
所以绑定的过程世纪上是将端口号和SOCK对象进行关联的过程,关联后,就可以以port(pid)为键值,调用netlink_getsockbyportid,查找struct sock对象,完成数据收发了。
netlink报文组织
unicast VS broadcast
kernel socket通过调用netlink_broadcast/netlink_unicast两个API向用户空间进行广播/单播通信。
sk_buff封装
参考文章
如何实现自定义sk_buff数据包并提交协议栈_13281771的技术博客_51CTO博客
sk_buff封装和解封装网络数据包的过程详解_protocol 0800 is buggy, dev-优快云博客
https://zhuanlan.zhihu.com/p/703843307
netlink机制详解_netlink通信枷锁示例-优快云博客
End