- ubuntu12.04环境下使用kvm ioctl接口实现最简单的虚拟机
- Ubuntu 通过无线网络安装Ubuntu Server启动系统后连接无线网络的方法
- 在Ubuntu上搭建网桥的方法
- ubuntu 虚拟机上网方式及相关配置详解
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界.
这篇CFSDN的博客文章彻底弄懂 Linux 网络命名空间(动手实验+源码分析)由作者收集整理,如果你对这篇文章有兴趣,记得点赞哟.
。
大家好,我是飞哥.
在 Linux 上通过 veth 我们可以创建出许多的虚拟设备。通过 Bridge 模拟以太网交换机的方式可以让这些网络设备之间进行通信。不过虚拟化中还有很重要的一步,那就是隔离。借用 Docker 的概念来说,那就是不能让 A 容器用到 B 容器的设备,甚至连看一眼都不可以。只有这样才能保证不同的容器之间复用硬件资源的同时,还不会影响其它容器的正常运行.
在 Linux 上实现隔离的技术手段就是 namespace。通过 namespace 可以隔离容器的进程 PID、文件系统挂载点、主机名等多种资源。不过我们今天重点要介绍的是网络 namespace,简称 netns。它可以为不同的命名空间从逻辑上提供独立的网络协议栈,具体包括网络设备、路由表、arp表、iptables、以及套接字(socket)等。使得不同的网络空间就都好像运行在独立的网络中一样.
你是不是和飞哥一样,也很好奇 Linux 底层到底是如何实现网络隔离的?我们今天来好好挖一挖 netns 的内部实现.
由于我们平时的开发工作很少涉及网络空间,所以我们先来看一下网络空间是如何使用的吧。我们来创建一个新的命名空间net1。再创建一对儿 veth,将 veth 的一头放到 net1 中。分别查看一下母机和 net1 空间内的 iptable、设备等。最后让两个命名空间之间进行通信.
下面是详细的使用过程。首先我们先来创建一个新的网络命名空间 - net1.
来查看一下它的 iptable、路由表、以及网络设备 。
由于是新创建的 netns,所以上述的输出中路由表、iptable规则都是空的。不过这个命名空间中初始的情况下就存在一个 lo 本地环回设备,只不过默认是 DOWN(未启动)状态.
接下来我们创建一对儿 veth,并把 veth 的一头添加给它.
在母机上查看一下当前的设备,发现已经看不到 veth1 这个网卡设备了,只能看到 veth1_p.
这个新设备已经跑到 net1 这个网络空间里了.
把这对儿 veth 分别配置上 ip,并把它们启动起来 。
在母机和 net1 中分别执行 ifconfig 查看当前启动的网络设备.
我们来让它和母机通信一下试试.
好了,现在一个我络命名空间创建实验就结束了。在这个空间里,网络设备、路由表、arp表、iptables都是独立的,不会和母机上的冲突,也不会和其它空间里的产生干扰。而且还可以通过 veth 来和其它空间下的网络进行通信.
想快速做这个实验的同学可以使用我写的一个makefile,见 https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/network/test05 。
在内核中,很多组件都是和 namespace 有关系的,我们先来看看这个关联关系是如何定义的。后面我们再看下 namespace 本身的详细结构.
在 Linux 中,很多我们平常熟悉的概念都是归属到某一个特定的网络 namespace 中的,比如进程、网卡设备、socket 等等.
Linux 中每个进程(线程)都是用 task_struct 来表示的。每个 task_struct 都要关联到一个 namespace 对象 nsproxy,而 nsproxy 又包含了 netns。对于网卡设备和 socket 来说,通过自己的成员来直接表明自己的归属.
拿网络设备来举例,只有归属到当前 netns 下的时候才能够通过 ifconfig 看到,否则是不可见的。我们详细来看看这几个数据结构的定义,先来看进程.
命名空间的核心数据结构是上面的这个 struct nsproxy。所有类型的 namespace(包括 pid、文件系统挂载点、网络栈等等)都是在这里定义的.
其中 struct net *net_ns 就是今天我们要讨论的网络命名空间。它的详细定义我们待会再说。我们接着再看表示网络设备的 struct net_device,它也是要归属到某一个网络空间下的.
所有的网络设备刚创建出来都是在宿主机默认网络空间下的。可以通过ip link set 设备名 netns 网络空间名将设备移动到另外一个空间里去。前面的实验里,当 veth 1 移动到 net1 下的时候,该设备在宿主机下“消失”了,在 net1 下就能看到了.
还有我们经常用的 socket,也是归属在某一个网络命名空间下的.
本小节中,我们来看网络 namespace 的主要数据结构 struct net 的定义.
可见每个 net 下都包含了自己的路由表、iptable 以及内核参数配置等等。我们来看具体的代码.
由上述定义可见,每一个 netns 中都有一个 loopback_dev,这就是为什么我们在第一节中看到刚创建出来的空间里就能看到一个 lo 设备的底层原因.
网络 netspace 中最核心的数据结构是 struct netns_ipv4 ipv4。在这个数据结构里,定义了每一个网络空间专属的路由表、ipfilter 以及各种内核参数.
回顾第一小节中,我们实验步骤主要是创建了一个 netns,为其添加了一个 veth 设备。在这节中我们来窥探一下刚才的实验步骤内部到底是如何运行的.
Linux 上存在一个默认的网络命名空间,Linux 中的 1 号进程初始使用该默认空间。Linux 上其它所有进程都是由 1 号进程派生出来的,在派生 clone 的时候如果没有额外特别指定,所有的进程都将共享这个默认网络空间.
在 clone 里可以指定创建新进程时的 flag,都是 CLONE_ 开头的。和 namespace 有的的标志位有 CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID 等等。如果在创建进程时指定了 CLONE_NEWNET 标记位,那么该进程将会创建并使用新的 netns.
其实内核提供了三种操作命名空间的方式,分别是 clone、setns 和 unshare。本文中我们只用 clone 来举例,ip netns add 使用的是 unshare,原理和 clone 是类似的.
我们先来看下默认的网络命名空间的初始化过程.
上面的代码是在初始化第 1 号进程。可见 nsproxy 是已经创建好的 init_nsproxy。再看 init_nsproxy 是如何创建的.
初始的 init_nsproxy 里将多个命名空间都进行了初始化,其中我们关注的网络命名空间,用的是默认网络空间 init_net。它是系统初始化的时候就创建好的.
上面的 setup_net 方法中对这个默认网络命名空间进行初始化.
看到这里我们清楚了 1 号进程的命名空间初始化过程。Linux 中所有的进程都是由这个 1 号进程创建的。如果创建子进程过程中没有指定 CLONE_NEWNET 这个 flag 的话,就直接还使用这个默认的网络空间.
如果创建进程过程中指定了 CLONE_NEWNET,那么就会重新申请一个网络命名空间出来。见如下的关键函数 copy_net_ns(它的调用链是 do_fork => copy_process => copy_namespaces => create_new_namespaces => copy_net_ns).
记住 setup_net 是初始化网络命名空间的,这个函数接下来我们还会提到.
命名空间内的各个组件都是在 setup_net 时初始化的,包括路由表、tcp 的 proc 伪文件系统、iptable 规则读取等等,所以这个小节也是蛮重要的.
由于内核网络模块的复杂性,在内核中将网络模块划分成了各个子系统。每个子系统都定义了一个 。
各个子系统通过调用 register_pernet_subsys 或 register_pernet_device 将其初始化函数注册到网络命名空间系统的全局链表 pernet_list 中。你在源码目录下用这两个函数搜索的话,会看到各个子系统的注册过程.
拿 register_pernet_subsys 来举例,我们来简单看下它是如何将子系统都注册到 pernet_list 中的.
register_pernet_operations 又会调用 __register_pernet_operations.
在上面 list_add_tail 这一行,完成了将子系统传入的 struct pernet_operations *ops 链入到 pernet_list 中。并注意一下,for_each_net 是遍历了所有的网络命名空间,然后在这个空间内执行了 ops_init 初始化.
这个初始化是网络子系统在注册的时候调用的。同样当新的命名空间创建时,会遍历该全局变量 pernet_list,执行每个子模块注册上来的初始化函数。再回到我们 3.1.1 里提到的 setup_net 函数.
在创建新命名空间调用到 setup_net 时,会通过 pernet_list 找到所有的网络子系统,把它们都 init 一遍.
我们拿路由表来举例,路由表子系统通过 register_pernet_subsys 将 fib_net_ops 注册进来了.
这样每当创建一个新的命名空间的时候,就会调用 fib_net_init 来创建一套独立的路由规则.
再比如拿 iptable 中的 nat 表来说,也是一样。每当创建新命名空间的时候,就会调用 iptable_nat_net_init 创建一套新的表.
在一个设备刚刚创建出来的时候,它是属于默认网络命名空间 init_net 的,包括 veth 设备。不过可以在创建完后修改设备到新的网络命名空间.
拿 veth 设备来举例,它是在创建时的源码 alloc_netdev_mqs 中设置到 init_net 上的。(执行代码路径:veth_newlink => rtnl_create_link => alloc_netdev_mqs) 。
在执行 修改设备所属的 namespace 的时候,会将 dev->nd_net 再指向新的 netns。对于 veth 来说,它包含了两个设备。这两个设备可以放在不同的 namespace 中。这就是 Docker 容器和其母机或者其它容器通信的基础.
在前面一节中,我们知道了内核是如何创建 netns 出来,也了解了网络设备是如何添加到其它命名空间里的。在这一小节,我们聊聊,当考虑到网络命名空间的时候,网络包的收发又是怎么样的呢?
首先来考虑的就是我们熟悉的 socket。其实每个 socket 都是归属于某一个网络命名空间的,这个关联关系在上面的 2.1 小节提到过.
到底归属那个 netns,这是由创建这个 socket 的进程所属的 netns 来决定。当在某个进程里创建 socket 的时候,内核就会把当前进程的 nsproxy->net_ns 找出来,并把它赋值给 socket 上的网络命名空间成员 skc_net.
在默认下,我们创建的 socket 都属于默认的网络命名空间 init_net 。
我们来展开看下 socket 是如何被放到某个网络命名空间中的。在 socket 中,用来保存和网络命名空间归属关系的变量是 skc_net,如下.
接下来就是 socket 创建的时候,内核中可以通过 current->nsproxy->net_ns 把当前进程所属的 netns 找出来,最终把 socket 中的 sk_net 成员和该命名空间建立好了联系.
在 socket_create 中,看到 current->nsproxy->net_ns 了吧,它获取到了进程的 netns。再依次经过__sock_create => inet_create => sk_alloc,调用到 sock_net_set 的时候,成功设置了新 socket 和 netns 的关联关系.
网络包的接收和发送过程我们在这两篇文章里详细介绍过,图解Linux网络包接收过程 和 25 张图,一万字,拆解 Linux 网络包发送过程.
本小节的不再重复赘述这个收发过程,我们就以网络包发送过程中的路由功能为例,来看一下网络在传输的时候是如何使用到 netns 的。其它收发过程中的各个步骤也都是类似的.
大致的原理就是 socket 上记录了其归属的网络命名空间。需要查找路由表之前先找到该命名空间,再找到命名空间里的路由表,然后再开始执行查找。这样,各个命名空间中的路由过程就都隔离开了.
我们来看详细的路由查找源码。在25 张图,一万字,拆解 Linux 网络包发送过程 中我们提到过在发送过程中在 IP 层的发送函数 ip_queue_xmit 中调用 ip_route_output_ports 来查找路由项.
注意上面的 sock_net(sk) 这一步,在这里将 socket 上记录的命名空间 struct net *sk_net 给找了出来.
找到命名空间以后,就会将它(以 struct net * 指针的形式)一路透传到后面的各个函数中。在127.0.0.1 之本机网络通信过程知多少 ?! 中我们介绍了路由查找最后会执行到 fib_lookup,我们来看下这个函数的源码.
路由查找的调用链条有点长,是 ip_route_output_ports => ->ip_route_output_flow => __ip_route_output_key() => ip_route_output_key_hash => ip_route_output_key_hash_rcu) 。
由上述代码可见,在路由过程中是根据前面步骤中确定好的命名空间 struct net *net 来查找路由项的。不同的命名空间有不同的 net 变量,所以不同的 netns 中自然也就可以配置不同的路由表了.
网络收发过程中其它步骤也是类似的,涉及到需要隔离的地方,都是通过命名空间( struct net *) 去查找的.
Linux 的网络 namespace 实现了独立协议栈的隔离。这个说法其实不是很准确,内核网络代码只有一套,并没有隔离.
它是通过为不同空间创建不同的 struct net 对象。每个 struct net 中都有独立的路由表、iptable 等数据结构。每个设备、每个 socket 上也都有指针指明自己归属那个 netns。通过这种方法从逻辑上看起来好像是真的有多个协议栈一样.
这样,就为一台物理上创建出多个逻辑上的协议栈,为 Docker 容器的诞生提供了可能.
在上面的示例中,Docker1 和 Docker2 都可以分别拥有自己独立的网卡设备,配置自己的路由规则、iptable。从而使得他们的网络功能不会相互影响.
怎么样,今天是不是对网络 namespace 理解更深了呢?
原文链接:https://mp.weixin.qq.com/s/lscMpc5BWAEzjgYw6H0wBw 。
最后此篇关于彻底弄懂 Linux 网络命名空间(动手实验+源码分析)的文章就讲到这里了,如果你想了解更多关于彻底弄懂 Linux 网络命名空间(动手实验+源码分析)的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于 Stack Overflow 来说是偏离主题的,
Linux 管道可以缓冲多少数据?这是可配置的吗? 如果管道的两端在同一个进程中,但线程不同,这会有什么不同吗? 请注意:这个“同一个进程,两个线程”的问题是理论上的边栏,真正的问题是关于缓冲的。 最
我找到了here [最后一页] 一种有趣的通过 Linux 启动 Linux 的方法。不幸的是,它只是被提及,我在网上找不到任何有用的链接。那么有人听说过一种避免引导加载程序而使用 Linux 的方法
很难说出这里要问什么。这个问题模棱两可、含糊不清、不完整、过于宽泛或夸夸其谈,无法以目前的形式得到合理的回答。如需帮助澄清此问题以便重新打开,visit the help center . 关闭 1
我试图了解 ld-linux.so 如何在 Linux 上解析对版本化符号的引用。我有以下文件: 测试.c: void f(); int main() { f(); } a.c 和 b.c:
与 RetroPie 的工作原理类似,我可以使用 Linux 应用程序作为我的桌面环境吗?我实际上并不需要像实际桌面和安装应用程序这样的东西。我只需要一种干净简单的方法来在 RaspberryPi 上
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 这个问题似乎不是关于 a specific programming problem, a softwar
关闭。这个问题是off-topic .它目前不接受答案。 想改进这个问题吗? Update the question所以它是on-topic用于堆栈溢出。 关闭 10 年前。 Improve thi
有什么方法可以覆盖现有的源代码,我应该用 PyQt、PyGTK、Java 等从头开始构建吗? 最佳答案 如果您指的是软件本身而不是它所连接的存储库,那么自定义应用程序的方法就是 fork 项目。据我所
我的情况是:我在一个磁盘上安装了两个 linux。我将第一个安装在/dev/sda1 中,然后在/dev/sda2 中安装第二个然后我运行第一个系统,我写了一个脚本来在第一个系统运行时更新它。
我在 i2c-0 总线上使用地址为 0x3f 的系统监视器设备。该设备在设备树中配置有 pmbus 驱动程序。 问题是,加载 linux 内核时,这个“Sysmon”设备没有供电。因此,当我在总线 0
关闭。这个问题是off-topic .它目前不接受答案。 想改进这个问题吗? Update the question所以它是on-topic用于堆栈溢出。 关闭 11 年前。 Improve thi
我正试图在 linux 模块中分配一大块内存,而 kalloc 做不到。 我知道唯一的方法是使用 alloc_bootmem(unsigned long size) 但我只能从 linux 内核而不是
关闭。这个问题不符合Stack Overflow guidelines .它目前不接受答案。 这个问题似乎不是关于 a specific programming problem, a softwar
我有 .sh 文件来运行应用程序。在该文件中,我想动态设置服务器名称,而不是每次都配置。 我尝试了以下方法,它在 CentOS 中运行良好。 nohup /voip/java/jdk1.8.0_71/
我是在 Linux 上开发嵌入式 C++ 程序的新手。我有我的 Debian 操作系统,我在其中开发和编译了我的 C++ 项目(一个简单的控制台进程)。 我想将我的应用程序放到另一个 Debian 操
关闭。这个问题需要多问focused 。目前不接受答案。 想要改进此问题吗?更新问题,使其仅关注一个问题 editing this post . 已关闭 4 年前。 Improve this ques
我使用4.19.78版本的稳定内核,我想找到带有企鹅二进制数据的C数组。系统启动时显示。我需要在哪里搜索该内容? 我在 include/linux/linux_logo.h 文件中只找到了一些 Log
我知道可以使用 gdb 的服务器模式远程调试代码,我知道可以调试针对另一种架构交叉编译的代码,但是是否可以更进一步,从远程调试 Linux 应用程序OS X 使用 gdbserver? 最佳答案 当然
是否有任何可能的方法来运行在另一个 Linux 上编译的二进制文件?我知道当然最简单的是在另一台机器上重建它,但假设我们唯一能得到的是一个二进制文件,那么这可能与否? (我知道这可能并不容易,但我只是
我是一名优秀的程序员,十分优秀!