浅谈K8S cni和网络方案

此文已由作者黄扬授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

在早先的k8s版本中,kubelet代码里提供了networkPlugin,networkPlugin是一组接口,实现了pod的网络配置、解除、获取,当时kubelet的代码中有个一个docker_manager,负责容器的创建和销毁,亦会负责容器网络的操作。而如今我们可以看到基本上kubelet的启动参数中,networkPlugin的值都会设置为cni。

cni插件的使用方式

使用CNI插件时,需要做三个配置:

  • kubelet启动参数中networkPlugin设置为cni
  • 在/etc/cni/net.d中增加cni的配置文件,配置文件中可以指定需要使用的cni组件及参数
  • 将需要用到的cni组件(二进制可执行文件)放到/opt/cni/bin目录下

所有的cni组件都支持两个命令:add和del。即配置网络和解除网络配置。

cni插件的配置文件是一个json文件,不同版本的接口、以及不同的cni组件,有着不同的配置内容结构,目前比较通用的接口版本是0.3.1的版本。

在配置文件中我们可以填入多个cni组件,当这些cni组件的配置以数组形式记录时,kubelet会对所有的组件进行按序链式调用,所有组件调用成功后,视为网络配置完成,过程中任何一步出现error,都会进行回滚的del操作。以保证操作流上的原子性。

几种基本的cni插件

cni插件按照代码中的存放目录可以分为三种:ipam、main、meta。

  • ipam cni用于管理ip和相关网络数据,配置网卡、ip、路由等。
  • main cni用于进行网络配置,比如创建网桥,vethpair、macvlan等。
  • meta cni有的是用于和第三方CNI插件进行适配,如flannel,也有的用于配置内核参数,如tuning

由于官方提供的cni组件就有很多,这里我们详细介绍一些使用率较高的组件。

ipam类CNI

ipam类型的cni插件,在执行add命令时会分配一个IP给调用者。执行del命令时会将调用者指定的ip放回ip池。社区开源的ipam有host-local、dhcp。

host-local

我们可以通过host-local的配置文件的数据结构来搞懂这个组件是如何管理ip的。

type IPAMConfig struct {
    *Range
    Name       string
    Type       string         `json:"type"`
    Routes     []*types.Route `json:"routes"`//交付的ip对应的路由
    DataDir    string         `json:"dataDir"`//本地ip池的数据库目录
    ResolvConf string         `json:"resolvConf"`//交付的ip对应的dns
    Ranges     []RangeSet     `json:"ranges"`//交付的ip所属的网段,网关信息
    IPArgs     []net.IP       `json:"-"` // Requested IPs from CNI_ARGS and args
}

#配置文件范例:
{
    "cniVersion": "0.3.1",
    "name": "mynet",
    "type": "ipvlan",
    "master": "foo0",
    "ipam": {
        "type": "host-local",
        "resolvConf": "/home/here.resolv",
        "dataDir": "/home/cni/network",
        "ranges": [
            [
                {
                    "subnet": "10.1.2.0/24",
                    "rangeStart": "10.1.2.9",
                    "rangeEnd": "10.1.2.20",
                    "gateway": "10.1.2.30"
                },
                {
                    "subnet": "10.1.4.0/24"
                }
            ],
            [{
                "subnet": "11.1.2.0/24",
                "rangeStart": "11.1.2.9",
                "rangeEnd": "11.1.2.20",
                "gateway": "11.1.2.30"
            }]
        ]
    }
}

从上面的配置我们可以清楚:

  • host-local组件通过在配置文件中指定的subnet进行网络划分
  • host-local在本地通过指定目录(默认为/var/lib/cni/networks)记录当前的ip pool数据
  • host-local将IP分配并告知调用者时,还可以告知dns、路由等配置信息。这些信息通过配置文件和对应的resolv文件记录。

host-local的应用范围比较广,kubenet、bridge、ptp、ipvlan等cni network插件都被用来和host-local配合进行ip管理。

dhcp

社区的cni组件中就包含了dhcp这个ipam,但并没有提供一个可以参考的案例,翻看了相关的源码,大致逻辑是:

  • 向dhcp申请ip时,dhcp会使用rpc访问本地的socket(/run/cni/dhcp.sock)申请一个ip的租约。然后将IP告知调用者。
  • 向dhcp删除IP时,dhcp同样通过rpc请求,解除该IP的租约。

main(network)类CNI

main类型的cni组件做的都是一些核心功能,比如配置网桥、配置各种虚拟化的网络接口(veth、macvlan、ipvlan等)。这里我们着重讲使用率较高的bridge和ptp。

bridge

brige模式,即网桥模式。在node上创建一个linux bridge,并通过vethpair的方式在容器中设置网卡和IP。只要为容器配置一个二层可达的网关:比如给网桥配置IP,并设置为容器ip的网关。容器的网络就能建立起来。

如下是bridge的配置项数据结构:

type NetConf struct {
    types.NetConf
    BrName       string `json:"bridge"` //网桥名
    IsGW         bool   `json:"isGateway"`  //是否将网桥配置为网关
    IsDefaultGW  bool   `json:"isDefaultGateway"` //
    ForceAddress bool   `json:"forceAddress"`//如果网桥已存在且已配置了其他IP,通过此参数决定是否将其他ip除去
    IPMasq       bool   `json:"ipMasq"`//如果true,配置私有网段到外部网段的masquerade规则
    MTU          int    `json:"mtu"`
    HairpinMode  bool   `json:"hairpinMode"`
    PromiscMode  bool   `json:"promiscMode"`
}

我们关注其中的一部分字段,结合代码可以大致整理出bridge组件的工作内容。首先是ADD命令:

  • 执行ADD命令时,brdige组件创建一个指定名字的网桥,如果网桥已经存在,就使用已有的网桥;
  • 创建vethpair,将node端的veth设备连接到网桥上;
  • 从ipam获取一个给容器使用的ip数据,并根据返回的数据计算出容器对应的网关;
  • 进入容器网络名字空间,修改容器中网卡名和网卡ip,以及配置路由,并进行arp广播(注意我们只为vethpair的容器端配置ip,node端是没有ip的);
  • 如果IsGW=true,将网桥配置为网关,具体方法是:将第三步计算得到的网关IP配置到网桥上,同时根据需要将网桥上其他ip删除。最后开启网桥的ip_forward内核参数;
  • 如果IPMasq=true,使用iptables增加容器私有网网段到外部网段的masquerade规则,这样容器内部访问外部网络时会进行snat,在很多情况下配置了这条路由后容器内部才能访问外网。(这里代码中会做exist检查,防止生成重复的iptables规则);
  • 配置结束,整理当前网桥的信息,并返回给调用者。

其次是DEL命令:

  • 根据命令执行的参数,确认要删除的容器ip,调用ipam的del命令,将IP还回IP pool;
  • 进入容器的网络名字空间,根据容器IP将对应的网卡删除;
  • 如果IPMasq=true,在node上删除创建网络时配置的几条iptables规则。

ptp

ptp其实是bridge的简化版。但是它做的网络配置其实看上去倒是更复杂了点。并且有一些配置在自测过程中发现并没有太大用处。它只创建vethpair,但是会同时给容器端和node端都配置一个ip。容器端配置的是容器IP,node端配置的是容器IP的网关(/32),同时,容器里做了一些特殊配置的路由,以满足让容器发出的arp请求能被vethpair的node端响应。实现内外的二层连通。

ptp的网络配置步骤如下:

  • 从ipam获取IP,根据ip类型(ipv4或ipv6)配置响应的内核ip_forward参数;
  • 创建一对vethpair;一端放到容器中;
  • 进入容器的网络namespace,配置容器端的网卡,修改网卡名,配置IP,并配置一些路由。假如容器ip是10.18.192.37/20,所属网段是10.18.192.0/20,网关是10.18.192.1,我们这里将进行这样的配置:
    • 配置IP后,内核会自动生成一条路由,形如:10.18.192.0/20 dev eth0 scope link,我们将它删掉:ip r d ****
    • 配置一条私有网到网关的真实路由:ip r a 10.18.192.0/20 via 10.18.192.1 dev eth0
    • 配置一条到网关的路由:10.18.192.1/32 dev eth0 scope link
  • 退出到容器外,将vethpair的node端配置一个IP(ip为容器ip的网关,mask=32);
  • 配置外部的路由:访问容器ip的请求都路由到vethpair的node端设备去。
  • 如果IPMasq=true,配置iptables
  • 获取完整的网卡信息(vethpair的两端),返回给调用者。

与bridge不同主要的不同是:ptp不使用网桥,而是直接使用vethpair+路由配置,这个地方其实有很多其他的路由配置可以选择,一样可以实现网络的连通性,ptp配置的方式只是其中之一。万变不离其宗的是:

只要容器内网卡发出的arp请求,能被node回复或被node转发并由更上层的设备回复,形成一个二层网络,容器里的数据报文就能被发往node上;然后通过node上的路由,进行三层转发,将数据报文发到正确的地方,就可以实现网络的互联。

bridge和ptp其实是用了不同方式实现了这个原则中的“二层网络”:

  • bridge组件给网桥配置了网关的IP,并给容器配置了到网关的路由。实现二层网络
  • ptp组件给vethpair的对端配置了网关的IP,并给容器配置了单独到网关IP的路由,实现二层网络

ptp模式的路由还存在一个问题:没有配置default路由,因此容器不能访问外部网络,要实现也很简单,以上面的例子,在容器里增加一条路由:default via 10.18.192.1 dev eth0

host-device

相比前面两种cni main组件,host-device显得十分简单因为他就只会做两件事情:

  • 收到ADD命令时,host-device根据命令参数,将网卡移入到指定的网络namespace(即容器中)。
  • 收到DEL命令时,host-device根据命令参数,将网卡从指定的网络namespace移出到root namespace。

细心的你肯定会注意到,在bridge和ptp组件中,就已经有“将vethpair的一端移入到容器的网络namespace”的操作。那这个host-device不是多此一举吗?

并不是。host-device组件有其特定的使用场景。假设集群中的每个node上有多个网卡,其中一个网卡配置了node的IP。而其他网卡都是属于一个网络的,可以用来做容器的网络,我们只需要使用host-device,将其他网卡中的某一个丢到容器里面就行。

host-device模式的使用场景并不多。它的好处是:bridge、ptp等方案中,node上所有容器的网络报文都是通过node上的一块网卡出入的,host-device方案中每个容器独占一个网卡,网络流量不会经过node的网络协议栈,隔离性更强。缺点是:在node上配置数十个网卡,可能并不好管理;另外由于不经过node上的协议栈,所以kube-proxy直接废掉。k8s集群内的负载均衡只能另寻他法了。

macvlan

有关macvlan的实践可以参考这篇文章。这里做一个简单的介绍:macvlan是linux kernal的特性,用于给一个物理网络接口(parent)配置虚拟化接口,虚拟化接口与parent网络接口拥有不同的mac地址,但parent接口上收到发给其对应的虚拟化接口的mac的包时,会分发给对应的虚拟化接口,有点像是将虚拟化接口和parent接口进行了‘桥接‘。给虚拟化网络接口配置了IP和路由后就能互相访问。

macvlan省去了linux bridge,但是配置macvlan后,容器不能访问parent接口的IP。

ipvlan

ipvlan与macvlan有点类似,但对于内核要求更高(3.19),ipvlan也会从一个网络接口创建出多个虚拟网络接口,但他们的mac地址是一样的, 只是IP不一样。通过路由可以实现不同虚拟网络接口之间的互联。

使用ipvlan也不需要linux bridge,但容器一样不能访问parent接口的IP。 关于ipvlan的内容可以参考这篇文章

关于macvlan和ipvlan,还可以参考这篇文章

meta 类CNI

meta组件通常进行一些额外的网络配置(tuning),或者二次调用(flannel)。

tuning

用于进行内核网络参数的配置。并将调用者的数据和配置后的内核参数返回给调用者。

有时候我们需要配置一些虚拟网络接口的内核参数,比如:网易云在早期经典网络方案中曾修改vethpair的proxy_arp参数(后面会介绍)。可以通过这个组件进行配置。 另外一些可能会改动的网络参数比如:

  • accept_redirects
  • send_redirects
  • proxy_delay
  • accept_local
  • arp_filter

可以在这里查看可配置的网络参数和释义。

portmap

用于在node上配置iptables规则,进行SNAT,DNAT和端口转发。

portmap组件通常在main组件执行完毕后执行,因为它的执行参数仰赖之前的组件提供

flannel

cni plugins中的flannel是开源网络方案flannel的“调用器”。这也是flannel网络方案适配CNI架构的一个产物。为了便于区分,以下我们称cni plugins中的flannel 为flanenl cni

我们知道flannel是一个容器的网络方案,通常使用flannel时,node上会运行一个daemon进程:flanneld,这个进程会返回该node上的flannel网络、subnet,MTU等信息。并保存到本地文件中。

如果对flannel网络方案有一定的了解,会知道他在做网络接口配置时,其实干的事情和bridge组件差不多。只不过flannel网络下的bridge会跟flannel0网卡互联,而flannel0网卡上的数据会被封包(udp、vxlan下)或直接转发(host-gw)。

flannel cni做的事情就是:

  • 执行ADD命令时,flannel cni会从本地文件中读取到flanneld的配置。然后根据命令的参数和文件的配置,生成一个新的cni配置文件(保存在本地,文件名包含容器id以作区分)。新的cni配置文件中会使用其他cni组件,并注入相关的配置信息。之后,flannel cni根据这个新的cni配置文件执行ADD命令。
  • 执行DEL命令时,flannel cni从本地根据容器id找到之前创建的cni配置文件,根据该配置文件执行DEL命令。

也就是说flannel cni此处是一个flannel网络模型的委托者,falnnel网络模型委托它去调用其他cni组件,进行网络配置。通常调用的是bridge和host-local。

几种常见的网络方案

上述所有的cni组件,能完成的事情就是建立容器到虚拟机上的网络。而要实现跨虚拟机的容器之间的网络,有几种可能的办法:

  • 容器的IP就是二层网络里分配的IP,这样容器相当于二层网络里的节点,那么就可以天然互访;
  • 容器的IP与node的IP不属于同一个网段,node上配置个到各个网段的路由(指向对应容器网段所部属的node IP),通过路由实现互访[flannel host-gw, calico bgp均是通过此方案实现];
  • 容器的IP与node的IP不属于同一个网段,node上有服务对容器发出的包进行封装,对发给容器的包进行解封。封装后的包通过node所在的网络进行传输。解封后的包通过网桥或路由直接发给容器,即overlay网络。[flannel udp/vxlan,calico ipip,openshift-sdn均通过此方案实现]

kubenet

了解常用的网络方案前,我们先了解一下kubenet,kubenet其实是k8s代码中内置的一个cni组件。如果我们要使用kubenet,就得在kubelet的启动参数中指定networkPlugin值为kubenet而不是cni

如果你阅读了kubernetes的源码,你就可以在一个名为kubenet_linux.go的文件中看到kubenet做了什么事情:

  • 身为一种networkPlugin,kubenet自然要实现networkPlugin的一些接口。比如SetUpPod,TearDownPod,GetPodNetworkStatus等等,kubelet通过这些接口进行容器网络的创建、解除、查询。
  • 身为一个代码中内置的cni,kubenet要主动生成一个cni配置文件(字节流数据),自己按照cni的规矩去读取配置文件,做类似ADD/DEL指令的工作。实现网络的创建、解除。

设计上其实挺蠢萌的。实际上是为了省事。我们可以看下自生成的配置文件:

{
  "cniVersion": "0.1.0",
  "name": "kubenet",
  "type": "bridge",
  "bridge": "%s", //通常这里默认是“cbr0”
  "mtu": %d,    //kubelet的启动参数中可以配置,默认使用机器上的最小mtu
  "addIf": "%s", //配置到容器中的网卡名字
  "isGateway": true,
  "ipMasq": false,
  "hairpinMode": %t, 
  "ipam": {
    "type": "host-local",
    "subnet": "%s", //node上容器ip所属子网,通常是kubelet的pod-cidr参数指定
    "gateway": "%s", //通过subnet可以确定gateway
    "routes": [
      { "dst": "0.0.0.0/0" }
    ]
  }
}

配置文件中明确了要使用的其他cni组件:bridge、host-local(这里代码中还会调用lo组件,通常lo组件会被k8s代码直接调用,所以不需要写到cni配置文件中)。之后的事情就是执行二进制而已。

为什么我们要学习kubenet?因为kubenet可以让用户以最简单的成本(配置networkPlugin和pod-cidr两个启动kubelet启动参数),配置出一个简单的、虚拟机本地的容器网络。结合上面提到的几种“跨虚拟机的容器之间的网络方案”,就是一个完整的k8s集群网络方案了。

通常kubenet不适合用于overlay网络方案,因为overlay网络方案定制化要求会比较高。

许多企业使用vpc网络时,使用自定义路由实现不同pod-cidr之间的路由,他们的网络方案里就会用到kubenet,比如azure AKS(基础网络)。

flannel

关于flannel,上面的文章也提到了一下。网上flannel的文章也是一搜一大把。这里简单介绍下flannel对k8s的支持,以及通用的几个flannel backend(后端网络配置方案)。

flannel for kubernetes

flannel在对kubernets进行支持时,flanneld启动参数中会增加--kube-subnet-mgr参数,flanneld会初始化一个kubernetes client,获取本地node的pod-cidr,这个pod-cidr将会作为flannel为node本地容器规划的ip网段。记录到/run/flannel/subnet.env。(flannel_cni组件会读取这个文件并写入到net-conf.json中,供cni使用)。

udp/vxlan

flannel的overlay方案。每个node节点上都有一个flanneld进程,和flannel0网桥,容器网络会与flannel0网桥互联,并经由flannel0发出,所以flanneld可以捕获到容器发出的报文,进行封装。udp方案下会给报文包装一个udp的头部,vxlan下会给报文包装一个vxlan协议的头部(配置了相同VNI的node,就能进行互联)。 目前flannel社区还提供了更多实验性的封装协议选择,比如ipip,但仍旧将vxlan作为默认的backend。

host-gw

flannel的三层路由方案。每个node节点上都会记录其他节点容器ip段的路由,通过路由,node A上的容器发给node B上的容器的数据,就能在node A上进行转发。

alloc

类似kubenet,只分配子网,不做其他任何事情。

支持云厂商的vpc

flannel支持了aliVPC、gce、aws等云厂商的vpc网络。原理都是一样的,就是当flanneld在某云厂商的机器上运行时,根据机器自身的vpc网络IP,和flanneld分配在该机器上的subnet,调用云厂商的api创建对应的自定义路由。

calico

calico是基于BGP路由实现的容器集群网络方案,对于使用者来说,基础的calico使用体验可能和flannel host-gw是基本一样的:node节点上做好对容器arp的响应。然后通过node上的路由将容器发出的包转发到对端容器所在node的IP。对端节点上再将包转发给对端容器。

ipip模式则如同flannel ipip模式。对报文封装一个ipip头部,头部中使用node ip。发送到对端容器所在node的IP,对端的网络组件再解包,并转发给容器。

不同之处在于flannel方案下路由都是通过代码逻辑进行配置。而calico则在每个节点建立bgp peer,bgp peer彼此之间会进行路由的共享和学习,所以自动生成并维护了路由。

点击可免费体验网易云容器服务

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 网易考拉Android客户端网络模块设计
【推荐】 质量评估面面观--聊一聊软件上线前的质量评估

原文地址:https://www.cnblogs.com/zyfd/p/10045238.html

时间: 2024-10-30 11:05:59

浅谈K8S cni和网络方案的相关文章

浅谈k8s:k8s部署架构以及工作原理

对于每个想落地kubernetes应用的工程师来说,熟悉kubernetes的架构和工作原理是必经之路,也是必须知道的知识,只有了解kubernetes的架构和工作原理才能更好的应用kubernetes . kubernetes架构 Kubernetes最初源于谷歌内部的Borg,提供了面向应用的容器集群部署和管理系统,所以整体的架构和borg很相似,整个架构有api server,control manager,scheduler,etcd,kubelet,kube-proxy,network

浅谈高并发架构

本篇文章主要是浅谈一些高并发的方案,指出一个大致方向,如果有需要优化提高系统性能,可以从以下方法中找出合适的使用. 随着淘宝.京东.唯品会等很多电商的出现,所谓互联网公司也就经常听到了,这些互联网公司给我们的第一印象,用户活跃交易量大.为了给用户一个好的交互体验,我们需要根据具体的业务场景来设计适合自己的高并发处理方案.服务器的架构我们在网上也看到过很多文档描述,像美团的火热.饿了么的崛起都有提到服务器的架构演变,基本都是从相对单一到集群,再到分布式服务.从一开始交易量小知名度低软件开发工期紧张

浅谈跨国网络传输

在这个大数据,云部署不断映入眼帘的时代,也许很多人作为公司IT架构的管理者都会觉得有些无助和迷惘.新兴的科技确实给日常的IT工作带来了便利,但亦带来了种种挑战和不可预期的困难. 数据的存储,传输的便利固然重要,但是数据的安全却要重要的多.你永远都不会希望把自己的核心数据放到公共的存储空间中,也随即诞生了私有云等一系列的概念,但是终究还是第三方的架构方案,这种不可控性随时都可发生. 对于跨国的数据传输,国内的网络提供商无论是电信和联通都无法给出完美的答案,因为国内伟大的防火墙的原因,速度慢之又慢,

搞懂分布式技术16:浅谈分布式锁的几种方案

搞懂分布式技术16:浅谈分布式锁的几种方案 前言 随着互联网技术的不断发展,数据量的不断增加,业务逻辑日趋复杂,在这种背景下,传统的集中式系统已经无法满足我们的业务需求,分布式系统被应用在更多的场景,而在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,在这种情况下,我们就需要用到分布式锁. 分布式一致性问题 首先我们先来看一个小例子: 假设某商城有一个商品库存剩10个,用户A想要买6个,用户B想要买5个,在理想状态下,用户A先买走了6了,库存减少6个还剩4个,

iOS浅谈如何进行网络判断

由于近段时间工作太忙,博客都有一段时间没有进行更新了,现在就来浅谈一下网络的判断,如有错误请各位大神能够指出来共同学习一下,谢谢!下面就进入正题了: 1.添加源文件(两个)下载地址:http://code4app.com/ios/Reachability/509743dc6803fae669000000 2.导入框架[SystemConfiguration.framework] 3.创建网络连接 3.1 创建互联网连接的对象 Reachability *reach1 = [Reachabilit

浅谈网络中的IP地址

IP地址是现在生活中不可或缺的,互联网的运用,使我们的生活变得多元化,充满乐趣.想了解这一切,需要先从根本了解,今天浅谈以下IP地址,从以下几个方面介绍: 一.IP地址的作用:在一定范围,唯一的标示,一个上网的设备:(凡是需要上网的设备,必须得有IP地址) 二. IP地址如何表示: 1.让机器看的10101010100010101010 (纯2进制) 2.让人看的点分十进制,X.X.X.X (X表示的是一个10进制)每一个X对应的是8个二进制每一个X对应 1 个字节:X取值范围是 0 --255

浅谈网络工程行业

网络工程,四个字听起来高端大气上档次,实际上背后有多少辛酸和喜悦为外行所了解. ---------------------题记 很多工科类大学和综合性大学都会有这样的一个专业叫网络工程,在彻底的外行人看来,网络工程就是一计算机类的专业,整天搞电脑:在普通的外行人看起来,网络工程就是it,it就是敲代码:在小年轻眼里,就是黑客:还有修电脑拉网线...每次听到这种说法,我心里都很无奈,因为从某种意义上说,在当今这个时代,网络工程是边缘产业,所以外行不懂太正常,但是我还是想写此文浅谈个人理解和感受.

k8s 各种网络方案 - 每天5分钟玩转 Docker 容器技术(170)

网络模型有了,如何实现呢? 为了保证网络方案的标准化.扩展性和灵活性,Kubernetes 采用了 Container Networking Interface(CNI)规范. CNI 是由 CoreOS 提出的容器网络规范,它使用了插件(Plugin)模型创建容器的网络栈. CNI 的优点是支持多种容器 runtime,不仅仅是 Docker.CNI 的插件模型支持不同组织和公司开发的第三方插件,这对运维人员来说很有吸引力,可以灵活选择适合的网络方案. 目前已有多种支持 Kubernetes

Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理

Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理 转自:https://www.jianshu.com/p/2b71ea919d49 本系列文章首发于我的个人博客:https://h2pl.github.io/ 欢迎阅览我的CSDN专栏:Java网络编程和NIO https://blog.csdn.net/column/details/21963.html 部分代码会放在我的的Github:https://github.com/h2pl/ 浅谈 Linux