容器技术Linux Namespaces和Cgroups

对操作系统了解多少,仅仅敲个命令吗

  1. 操作系统虚拟化(容器技术)的发展历程

1979 年,UNIX 的第 7 个版本引入了 Chroot 特性。Chroot 现在被认为是第一个操作系统虚拟化(Operating system level virtualization)技术的原型,本质是一种操作系统文件系统层的隔离技术。

2006 年,Google 发布了在 Linux 上运行的 Process Container(进程容器)技术,其目标是提供一种类似于 Virtual Mahine(计算机虚拟化技术)的、但主要针对 Process 的操作系统级别资源限制、优先级控制、资源审计能力和进程控制能力。

2007 年,Google 推动 Process Container 代码合入 Linux Kernel。同时由于 Container 这一命名在 Kernel 具有许多不同的含义,所以为了避免代码命名的混乱,就将 Process Container 更名为了 Control Groups,简称:Cgroups

2008 年,Linux 社区整合了 Chroot、Cgroups、Namespaces、SELinux、Seccomp 等多种技术并发布了 LXC(Linux Container)v0.1.0 版本。LXC 通过将 Cgroups 的资源配额管理能力和 Namespace 的资源视图隔离能力进行组合,实现了完备的轻量级操作系统虚拟化。

2013 年 3 月 15 日,在加利福尼亚州圣克拉拉召开的 Python 开发者大会上,DotCloud 的创始人兼首席执行官 Solomon Hvkes 在一场仅 5 分钟的微型演讲中,首次发布了基于 LXC 封装的 Docker Container,并于会后将其源码开源并托管到 Github。

图片

2.   容器的优势

传统模式的部署,直接将多个应用运行在物理服务器上,如果其中一个应用占用了大部分资源,可能会导致其他应用的性能下降。

虚拟化部署时代,可以在单个物理服务器的 CPU 上运行多个虚拟机(VM),每个 VM 是一台完整的计算机,在虚拟化硬件之上运行所有组件(包括了操作系统)。因此,可以让不同的应用在 VM 之间安全地隔离运行,更好地利用物理服务器上的资源。

容器与 VM 类似,具有自己的文件系统、CPU、内存、进程空间等,但与 VM 不同的是,容器之间共享操作系统(OS)。 所以,容器被认为是一种轻量级的操作系统层面的虚拟化技术。

相比于 VM ,轻量级的容器更适合云原生模式的实践。

 

 3.  容器的本质

容器是一种轻量级的操作系统层面的虚拟化技术。

重点是 “操作系统层面” ,即容器本质上是利用操作系统提供的功能来实现虚拟化。

容器技术的代表之作 Docker ,则是一个基于 Linux 操作系统,使用 Go 语言编写,调用了 Linux Kernel 功能的虚拟化工具。

为了更好地理解容器的本质,我们来看看容器具体使用了哪些 Linux Kernel 技术,以及在 Go 中应该如何去调用。

图片

3.1  Chroot

Chroot 是一个可供 User Process 调用的 System Call 接口,可以让一个 Process 把指定的目录作为根目录(Root Directory),随后 Process 所有的文件系统操作都只能在这个指定目录中进行。故称之为 Change Root。

chroot() 的函数原型非常简单:

  • 调用权限:Root 用户。

  • 形参列表

    • path:一个指向字符串的指针,是一个绝对路径,表示将 Process 的根目录更改为的该目录路径。

  • 函数返回

    • 成功:返回 0;

    • 失败:返回 -1。

#include <unistd.h>

int chroot(const char *path);

需要注意的是,在更改了 Process 的根目录后,Process 只能访问新的根目录以及其子目录中的文件和资源。因此,在调用 chroot() 后,应确保 Process 所需要访问的所有文件和资源都存在于新的根目录下。

chroot() 目前主要主要用于:

安全隔离场景:限制将 Process 的访问范围,以此提高系统的安全性。

调试环境场景:创建一个与主系统隔离的环境,用于调试、测试和运行 Process。

系统救援场景:在 Linux 操作系统损坏或遭受攻击时,可以使用 chroot 将 Process 切换到受损系统的根目录中,以便进行修复和救援操作。

可见,chroot() 确实在 Linux File System(文件系统)层面提供了针对 Process 的隔离性,但并不提供完全的安全隔离,无法阻止其他方式的攻击。因此,要想实现 Processes 之间的安全隔离,还需要需采取其他安全措施。

3.2  NameSpaces

Linux Namespaces(命名空间)是一种操作系统层级的资源视图隔离技术,能够将 Linux 的全局资源,划分为 Namespace 范围内可见的资源。

由于容器之间共享 OS ,对于操作系统而言,容器的实质就是进程,多个容器运行,对应操作系统也就是运行着多个进程。

当进程运行在自己单独的命名空间时,命名空间的资源隔离可以保证进程之间互不影响,大家都以为自己身处在独立的一个操作系统里。这种进程就可以称为容器。

Namespaces 具有多种类型,基本上涵盖了构成一个操作系统所需要的基本元素:

命名空间系统调用参数作用
Mount (mnt)CLONE_NEWNS文件目录挂载隔离。用于隔离各个进程看到的挂载点视图
Process ID (pid)CLONE_NEWPID进程 ID 隔离。使每个命名空间都有自己的初始化进程,PID 为 1,作为所有进程的父进程
Network (net)CLONE_NEWNET网络隔离。使每个 net 命名空间有独立的网络设备,IP 地址,路由表,/proc/net 目录等网络资源
Interprocess Communication (ipc)CLONE_NEWIPC进程 IPC 通信隔离。让只有相同 IPC 命名空间的进程之间才可以共享内存、信号量、消息队列通信
UTSCLONE_NEWUTS主机名或域名隔离。使其在网络上可以被视作一个独立的节点而非主机上的一个进程
User ID (user)CLONE_NEWUSER用户 UID 和组 GID 隔离。例如每个命名空间都可以有自己的 root 用户
Control group (cgroup) NamespaceCLONE_NEWCGROUPCgroup 信息隔离。用于隐藏进程所属的控制组的身份,使命名空间中的 cgroup 视图始终以根形式来呈现,保障安全
Time NamespaceCLONE_NEWTIME系统时间隔离。允许不同进程查看到不同的系统时间

通过 /proc/{pid}/ns 文件可以查看指定 Process 运行在哪些 Namespaces Instance 中,并且每个 Namespace Instance 都具有一个唯一的标识。

[root@192 dongguangming]#  ls -l --time-style='+' /proc/$$/ns
total 0
lrwxrwxrwx. 1 root root 0  ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0  mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0  net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0  pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0  user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0  uts -> uts:[4026531838]
[root@192 dongguangming]# 

最终,用户可以通过创建多种不同类型的 Namespaces Instance 来提供的操作系统资源的隔离,再结合创建多种不同类型的 cgroups 来提供操作系统的资源配额,就构成了一个最基本的操作系统容器,即:Process Container。

UTS namespace

UTS namespace 为 Container 提供了 Hostname 和 Domain Name 的隔离。

Container 中的 Process 可以根据需要调用 sethostname 和 setdomainname 指令来进行配置,让每个 Container 都可以被视为网络中的一个独立的节点。

PID namespace

PID namespace 为 Container 提供了进程号的隔离。

每个 Containers 都拥有自己的进程环境,Container 的 init Process 都是 PID 1 号进程,它作为所有子进程的父进程。要想做到进程的隔离,首先需要创建出 PID 1 号进程,它具有以下特性:

  • 如果某个子进程脱离了父进程(父进程没有 wait 它),那么 init Process 就会负责回收资源并结束这个子进程。

  • 如果 init Process  被终止,那么 Kernel 就会调用 SIGKILL 终止此 PID namespace 中的所有进程。

IPC namespace

IPC namespace 为 Container 提供了 IPC(进程间)通信机制的隔离,包括信号量、消息队列、共享内存等机制。

每个 Containers 都拥有以下 /proc 文件接口:

  • /proc/sys/fs/mqueue:POSIX Message Queues 接口类型;

  • /proc/sys/kernel:System V IPC 接口类型;

  • /proc/sysvipc:System V IPC 接口类型。

Mount namespace

Mount namespace 为 Container 提供了 Filesystem 挂载点的隔离,继而实现了 VFS 的隔离。

每个 Containers 都拥有以下 /proc 文件接口,可以构成一个独立的 rootfs(Root 文件系统):

  • /proc/[pid]/mounts

  • /proc/[pid]/mountinfo

  • /proc/[pid]/mountstats

实际上,Mount namespace 是基于 Chroot 的不断改良而开发出来的。为 Container 创建的 rootfs 只是一个操作系统发行版所包含的文件、目录和配置,并不包括 Kernel 的文件。

Network namespace

Network namespace 为 Container 提供了网络资源的隔离,包括:

  • Network devices(网络设备)

  • IPv4 and IPv6 protocol stacks(IPv4、IPv6 的协议栈)

  • IP routing tables(IP 路由表)

  • Firewall rules(防火墙规则)

  • Sockets 套接字

  • /proc/[pid]/net

  • /sys/class/net

  • /proc/sys/net

需要注意的是,同一个 Network device 只能存在于一个 Namespace Instance 中,所以常常结合虚拟网络设备来使用。

图片

User namespace

User namespace 为 Container 提供了用户权限和安全属性相关的隔离,包括:User ID、User Group ID、Root 目录以及特殊的权限。

每个 Containers 都拥有以下 /proc 文件接口:

  • /proc/[pid]/uid_map

  • /proc/[pid]/gid_map

NameSpace 的具体描述可以查看 Linux man 手册中的 namespaces(7) - Linux manual page,手册中还描述了几个 NameSpace API ,主要是和进程相关的系统调用函数。

图片

clone()
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
                 /* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

clone() 用于创建新进程,通过传入一个或多个系统调用参数( flags 参数)可以创建出不同类型的 NameSpace ,并且子进程也将会成为这些 NameSpace 的成员。

setns()
int setns(int fd, int nstype);

setns() 用于将进程加入到一个现有的 Namespace 中。其中 fd 为文件描述符,引用 /proc/[pid]/ns/ 目录里对应的文件,nstype 代表 NameSpace 类型。

unshare()
int unshare(int flags);

unshare() 用于将进程移出原本的 NameSpace ,并加入到新创建的 NameSpace 中。同样是通过传入一个或多个系统调用参数( flags 参数)来创建新的 NameSpace 。

ioctl()
int ioctl(int fd, unsigned long request, ...);

ioctl() 用于发现有关 NameSpace 的信息。

上面的这些系统调用函数,我们可以直接用 C 语言调用,创建出各种类型的 NameSpace ,这是最直观的做法。而对于 Go 语言,其内部已经帮我们封装好了这些函数操作,可以更方便地直接使用,降低心智负担。

先来看一个简单的小工具

package main

import (
 "os"
 "os/exec"
)

func main() {
 switch os.Args[1] {
 case "run":
  run()
 default:
  panic("help")
 }
}

func run() {
 cmd := exec.Command(os.Args[2], os.Args[3:]...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 must(cmd.Run())
}

func must(err error) {
 if err != nil {
  panic(err)
 }
}

 这个程序接收用户命令行传递的参数,并使用 exec.Command 运行,例如当我们执行 go run main.go run echo hello 时,会创建出 main 进程, main 进程内执行 echo hello 命令创建出一个新的 echo 进程,最后随着 echo 进程的执行完毕,main 进程也随之结束并退出。

但是上面创建的进程太快退出了,不便于我们观察。如果让 main 进程启动一个 bash 进程会怎样呢?

为了直观对比,我们先看看当前会话的进程信息。

[root@192 dongguangming]# ps
  PID TTY          TIME CMD
 1407 pts/1    00:00:00 bash
 2057 pts/1    00:00:00 ps
[root@192 dongguangming]# echo $$
1407
[root@192 dongguangming]# 

 当前我们正处于 PID 1407 的 bash 会话进程中,继续下一步操作:

[root@192 dongguangming]# go run main.go run /bin/bash
[root@192 dongguangming]# ps
  PID TTY          TIME CMD
 1407 pts/1    00:00:00 bash
 2058 pts/1    00:00:00 go
 2076 pts/1    00:00:00 main
 2079 pts/1    00:00:00 bash
 2089 pts/1    00:00:00 ps
[root@192 dongguangming]# echo $$
2079
[root@192 dongguangming]# exit
exit
[root@192 dongguangming]# ps
  PID TTY          TIME CMD
 1407 pts/1    00:00:00 bash
 2090 pts/1    00:00:00 ps
[root@192 dongguangming]# echo $$
1407
[root@192 dongguangming]# 

在执行 go run main.go run /bin/bash 后,我们的会话被切换到了 PID 2709的 bash 进程中,而 main 进程也还在运行着(当前所处的 bash 进程是 main 进程的子进程,main 进程必须存活着,才能维持 bash 进程的运行)。当执行 exit 退出当前所处的 bash 进程后,main 进程随之结束,并回到原始的 PID 1407的 bash 会话进程。

我们说过,容器的实质是进程,你现在可以把 main 进程当作是 “Docker” 工具,把 main 进程启动的 bash 进程,当作一个 “容器” 。这里的 “Docker” 创建并启动了一个 “容器”。

为什么打了双引号,是因为在这个 bash 进程中,我们可以随意使用操作系统的资源,并没有做资源隔离。

要想实现资源隔离,也很简单,在 run() 函数增加 SysProcAttr 配置,先从最简单的 UTS 隔离开始,传入对应的 CLONE_NEWUTS 系统调用参数,并通过 syscall.Sethostname 设置主机名:

func run() {
 cmd := exec.Command(os.Args[2], os.Args[3:]...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 cmd.SysProcAttr = &syscall.SysProcAttr{
  Cloneflags: syscall.CLONE_NEWUTS,
 }
 must(syscall.Sethostname([]byte("mycontainer")))
 must(cmd.Run())
}

 

这段代码看似没什么问题,但仔细思考一下。

syscall.Sethostname 这一行到底是哪个进程在执行?main 进程还是 main 进程创建的子进程?

不用想,子进程都还没 Run 起来呢!现在调用肯定是 main 进程在执行,main 进程可没进行资源隔离,相当于直接更改宿主机的主机名了。

子进程还没 Run 起来,还不能更改主机名,等子进程 Run 起来后,又会进入到阻塞状态,无法再通过代码方式更改到子进程内的主机名。那有什么办法呢?

看来只能把 /proc/self/exe 这个神器请出来了。

在 Linux 2.2 内核版本及其之后,/proc/[pid]/exe 是对应 pid 进程的二进制文件的符号链接,包含着被执行命令的实际路径名。如果打开这个文件就相当于打开了对应的二进制文件,甚至可以通过重新输入 /proc/[pid]/exe 重新运行一个对应于 pid 的二进制文件的进程。

对于 /proc/self ,当进程访问这个神奇的符号链接时,可以解析到进程自己的 /proc/[pid] 目录。

合起来就是,当进程访问 /proc/self/exe 时,可以运行一个对应进程自身的二进制文件。

这有什么用呢?继续看下面的代码:

package main

import (
 "os"
 "os/exec"
 "syscall"
)

func main() {
 switch os.Args[1] {
 case "run":
  run()
 case "child":
  child()
 default:
  panic("help")
 }
}

func run() {
 cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 cmd.SysProcAttr = &syscall.SysProcAttr{
  Cloneflags: syscall.CLONE_NEWUTS,
 }
 must(cmd.Run())
}

func child() {
 must(syscall.Sethostname([]byte("mycontainer")))
 cmd := exec.Command(os.Args[2], os.Args[3:]...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 must(cmd.Run())
}

func must(err error) {
 if err != nil {
  panic(err)
 }
}

 

在 run() 函数中,我们不再是直接运行用户所传递的命令行参数,而是运行 /proc/self/exe ,并传入 child 参数和用户传递的命令行参数。

同样当执行 go run main.go run echo hello 时,会创建出 main 进程, main 进程内执行 /proc/self/exe child echo hello 命令创建出一个新的 exe 进程,关键也就是这个 exe 进程,我们已经为其配置了 CLONE_NEWUTS 系统调用参数进行 UTS 隔离。也就是说,exe 进程可以拥有和 main 进程不同的主机名,彼此互不干扰。

进程访问 /proc/self/exe 代表着运行对应进程自身的二进制文件。因此,按照 exe 进程的启动参数,会执行 child() 函数,而 child() 函数内首先调用 syscall.Sethostname 更改了主机名(此时是 exe 进程执行的,并不会影响到 main 进程),接着和本文最开始的 run() 函数一样,再次使用 exec.Command 运行用户命令行传递的参数。

总结一下就是, main 进程创建了 exe 进程(exe 进程已经进行 UTS 隔离,exe 进程更改主机名不会影响到 main 进程), 接着 exe 进程内执行 echo hello 命令创建出一个新的 echo 进程,最后随着 echo 进程的执行完毕,exe 进程随之结束,exe 进程结束后, main 进程再结束并退出。

图片

那经过 exe 这个中间商所创建出来的 echo 进程和之前由 main 进程直接创建的 echo 进程,两者有何不同呢。

我们知道,创建 exe 进程的同时我们传递了 CLONE_NEWUTS 标识符创建了一个 UTS NameSpace ,Go 内部帮我们封装了系统调用函数 clone() 的调用,我们也说过,由 clone() 函数创建出的进程的子进程也将会成为这些 NameSpace 的成员,所以默认情况下(创建新进程时无继续指定系统调用参数),由 exe 进程创建出的 echo 进程会继承 exe 进程的资源, echo 进程将拥有和 exe 进程相同的主机名,并且同样和 main 进程互不干扰。

因此,借助中间商 exe 进程 ,echo 进程可以成功实现和宿主机( main 进程)资源隔离,拥有不同的主机名。

图片

再次通过启动 /bin/bash 进行验证主机名是否已经成功隔离:

[root@192 dongguangming]# hostname
192.168.0.103
[root@192 dongguangming]# touch main.go
[root@192 dongguangming]# vi main.go 
[root@192 dongguangming]# go run main.go run /bin/bash
[root@mycontainer dongguangming]# hostname
mycontainer
[root@mycontainer dongguangming]# ps
  PID TTY          TIME CMD
 1407 pts/1    00:00:00 bash
 2112 pts/1    00:00:00 go
 2133 pts/1    00:00:00 main1
 2136 pts/1    00:00:00 exe
 2140 pts/1    00:00:00 bash
 2150 pts/1    00:00:00 ps
[root@mycontainer dongguangming]# exit
exit
[root@192 dongguangming]# hostname
192.168.0.103
[root@192 dongguangming]# 

 当执行 go run main.go run /bin/bash 时,我们也可以在另一个 ssh 会话中,使用 ps afx 查看bash 会话进程的层次信息:

以此类推,新增资源隔离只要继续传递指定的系统调用参数即可:

package main

import (
 "fmt"
 "os"
 "os/exec"
 "syscall"
)

func main() {
 switch os.Args[1] {
 case "run":
  run()
 case "child":
  child()
 default:
  panic("help")
 }
}

func run() {
 fmt.Println("[main]", "pid:", os.Getpid())
 cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 cmd.SysProcAttr = &syscall.SysProcAttr{
  Cloneflags: syscall.CLONE_NEWUTS |
   syscall.CLONE_NEWPID |
   syscall.CLONE_NEWNS,
  Unshareflags: syscall.CLONE_NEWNS,
 }
 must(cmd.Run())
}

func child() {
 fmt.Println("[exe]", "pid:", os.Getpid())
 must(syscall.Sethostname([]byte("mycontainer")))
 must(os.Chdir("/"))
 must(syscall.Mount("proc", "proc", "proc", 0, ""))
 cmd := exec.Command(os.Args[2], os.Args[3:]...)
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 must(cmd.Run())
 must(syscall.Unmount("proc", 0))
}

func must(err error) {
 if err != nil {
  panic(err)
 }
}

 

Cloneflags 参数新增了 CLONE_NEWPID 和 CLONE_NEWNS 分别隔离进程 pid 和文件目录挂载点视图,Unshareflags: syscall.CLONE_NEWNS 则是用于禁用挂载传播(如果不设置该参数,container 内的挂载会共享到 host ,挂载传播不在本文的探讨范围内)。

当我们创建 PID Namespace 时,exe 进程包括其创建出来的子进程的 pid 已经和 main 进程隔离了,这一点可以通过打印 os.Getpid() 结果或执行 echo $$ 命令得到验证。但此时还不能使用 ps 命令查看,因为 ps 和 top 等命令会使用 /proc 的内容,所以我们才继续引入了 Mount Namespace ,并在 exe 进程挂载 /proc 目录。

[root@192 dongguangming]# ps
  PID TTY          TIME CMD
 1407 pts/1    00:00:00 bash
 2195 pts/1    00:00:00 ps
[root@192 dongguangming]# echo $$
1407
[root@192 dongguangming]# go run main.go run /bin/bash
[main] pid: 2217
[exe] pid: 1
[root@mycontainer /]# ps
  PID TTY          TIME CMD
    1 pts/1    00:00:00 exe
    4 pts/1    00:00:00 bash
   13 pts/1    00:00:00 ps
[root@mycontainer /]#  echo $$
4
[root@mycontainer /]# exit
exit
[root@192 dongguangming]# 

通过 /proc/{pid}/ns 文件可以查看指定 Process 运行在哪些 Namespaces Instance 中,并且每个 Namespace Instance 都具有一个唯一的标识。

此时,exe 作为初始化进程,pid 为 1 ,创建出了 pid 4 的 bash 子进程,而且已经看不到 main 进程了。

剩下的 IPC 、NET、 USER 等 NameSpace 就不在本文一一展示了。

3.3  Cgroups

借助 NameSpace 技术可以帮进程隔离出自己单独的空间,成功实现出最简容器。但是怎样限制这些空间的物理资源开销(CPU、内存、存储、I/O 等)就需要利用 Cgroups 技术了。

限制容器的资源使用,是一个非常重要的功能,如果一个容器可以毫无节制的使用服务器资源,那便又回到了传统模式下将应用直接运行在物理服务器上的弊端。这是容器化技术不能接受的。

Cgroups 的全称是 Control groups 即控制组,最早是由 Google 的工程师(主要是 Paul Menage 和 Rohit Seth)在 2006 年发起,一开始叫做进程容器(process containers)。在 2007 年时,因为在 Linux Kernel 中,容器(container)这个名词有许多不同的意义,为避免混乱,被重命名为 cgroup ,并且被合并到 2.6.24 版本的内核中去。

Cgroups 是对进程分组管理的一种机制,提供了对一组进程及它们的子进程的资源限制、控制和统计的能力,并为每种可以控制的资源定义了一个 subsystem (子系统)的方式进行统一接口管理,因此 subsystem 也被称为 resource controllers (资源控制器)。

几个主要的 subsystem 如下( Cgroups V1 ):

子系统作用
cpu限制进程的 cpu 使用率
cpuacct统计进程的 cpu 使用情况
cpuset在多核机器上为进程分配单独的 cpu 节点或者内存节点(仅限 NUMA 架构)
memory限制进程的 memory 使用量
blkio控制进程对块设备(例如硬盘) io 的访问
devices控制进程对设备的访问
net_cls标记进程的网络数据包,以便可以使用 tc 模块(traffic control)对数据包进行限流、监控等控制
net_prio控制进程产生的网络流量的优先级
freezer挂起或者恢复进程
pids限制 cgroup 的进程数量
更多子系统参考 Linux man cgroups[3]文档https://man7.org/linux/man-pages/man7/cgroups.7.html

借助 Cgroups 机制,可以将一组进程(task group)和一组 subsystem 关联起来,达到控制进程对应关联的资源的能力。如图:

图片

Cgroups 的层级结构称为 hierarchy (即 cgroup 树),是一棵树,由 cgroup 节点组成。

系统可以有多个 hierarchy ,当创建新的 hierarchy 时,系统所有的进程都会加入到这个 hierarchy 默认创建的 root cgroup 根节点中,在树中,子节点可以继承父节点的属性。

对于同一个 hierarchy,进程只能存在于其中一个 cgroup 节点中。如果把一个进程添加到同一个 hierarchy 中的另一个 cgroup 节点,则会从第一个 cgroup 节点中移除。

hierarchy 可以附加一个或多个 subsystem 来拥有对应资源(如 cpu 和 memory )的管理权,其中每一个 cgroup 节点都可以设置不同的资源限制权重,而进程( task )则绑定在 cgroup 节点中,并且其子进程也会默认绑定到父进程所在的 cgroup 节点中。

基于 Cgroups 的这些运作原理,可以得出:如果想限制某些进程的内存资源,就可以先创建一个 hierarchy ,并为其挂载 memory subsystem ,然后在这个 hierarchy 中创建一个 cgroup 节点,在这个节点中,将需要控制的进程 pid 和控制属性写入即可。

接下来我们就来实践一下。

Linux 一切皆文件。

在 Linux Kernel 中,为了让 Cgroups 的配置更直观,使用了目录的层级关系来模拟 hierarchy ,以此通过虚拟的树状文件系统的方式暴露给用户调用。

创建一个 hierarchy ,并为其挂载 memory subsystem ,这一步我们可以跳过,因为系统已经默认为每个 subsystem 创建了一个默认的 hierarchy ,我们可以直接使用。

例如 memory subsystem 默认的 hierarchy 就在 /sys/fs/cgroup/memory 目录。

[root@host go]# mount | grep memory
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
[root@host go]# cd /sys/fs/cgroup/memory
[root@host memory]# pwd
/sys/fs/cgroup/memory
[root@host memory]#

只要在这个 hierarchy 目录下创建一个文件夹,就相当于创建了一个 cgroup 节点:

[root@host memory]# mkdir hello
[root@host memory]# cd hello/
[root@host hello]# ls
cgroup.clone_children           memory.kmem.slabinfo                memory.memsw.failcnt             memory.soft_limit_in_bytes
cgroup.event_control            memory.kmem.tcp.failcnt             memory.memsw.limit_in_bytes      memory.stat
cgroup.procs                    memory.kmem.tcp.limit_in_bytes      memory.memsw.max_usage_in_bytes  memory.swappiness
memory.failcnt                  memory.kmem.tcp.max_usage_in_bytes  memory.memsw.usage_in_bytes      memory.usage_in_bytes
memory.force_empty              memory.kmem.tcp.usage_in_bytes      memory.move_charge_at_immigrate  memory.use_hierarchy
memory.kmem.failcnt             memory.kmem.usage_in_bytes          memory.numa_stat                 notify_on_release
memory.kmem.limit_in_bytes      memory.limit_in_bytes               memory.oom_control               tasks
memory.kmem.max_usage_in_bytes  memory.max_usage_in_bytes           memory.pressure_level
[root@host hello]#

其中我们创建的 hello 文件夹内的所有文件都是系统自动创建的。常用的几个文件功能如下:

文件名功能
taskscgroup 中运行的进程( PID)列表。将 PID 写入一个 cgroup 的 tasks 文件,可将此进程移至该 cgroup
cgroup.procscgroup 中运行的线程群组列表( TGID )。将 TGID 写入 cgroup 的 cgroup.procs 文件,可将此线程组群移至该 cgroup
cgroup.event_controlevent_fd() 的接口。允许 cgroup 的变更状态通知被发送
notify_on_release用于自动移除空 cgroup 。默认为禁用状态(0)。设定为启用状态(1)时,当 cgroup 不再包含任何任务时(即,cgroup 的 tasks 文件包含 PID,而 PID 被移除,致使文件变空),kernel 会执行 release_agent 文件(仅在 root cgroup 出现)的内容,并且提供通向被清空 cgroup 的相关路径(与 root cgroup 相关)作为参数
memory.usage_in_bytes显示 cgroup 中进程当前所用的内存总量(以字节为单位)
memory.memsw.usage_in_bytes显示 cgroup 中进程当前所用的内存量和 swap 空间总和(以字节为单位)
memory.max_usage_in_bytes显示 cgroup 中进程所用的最大内存量(以字节为单位)
memory.memsw.max_usage_in_bytes显示 cgroup 中进程的最大内存用量和最大 swap 空间用量(以字节为单位)
memory.limit_in_bytes设定用户内存(包括文件缓存)的最大用量
memory.memsw.limit_in_bytes设定内存与 swap 用量之和的最大值
memory.failcnt显示内存达到 memory.limit_in_bytes 设定的限制值的次数
memory.memsw.failcnt显示内存和 swap 空间总和达到 memory.memsw.limit_in_bytes 设定的限制值的次数
memory.oom_control可以为 cgroup 启用或者禁用“内存不足”(Out of Memory,OOM) 终止程序。默认为启用状态(0),尝试消耗超过其允许内存的任务会被 OOM 终止程序立即终止。设定为禁用状态(1)时,尝试使用超过其允许内存的任务会被暂停,直到有额外内存可用。
更多文件的功能说明可以查看 kernel 文档中的 cgroup-v1/memory[4]https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt

在这个 hello cgroup 节点中,我们想限制某些进程的内存资源,只需将对应的进程 pid 写入到 tasks 文件,并把内存最大用量设定到 memory.limit_in_bytes 文件即可。

[root@host hello]# cat memory.oom_control
oom_kill_disable 0
under_oom 0
[root@host hello]# cat memory.failcnt
0
[root@host hello]# echo 100M > memory.limit_in_bytes
[root@host hello]# cat memory.limit_in_bytes
104857600
[root@host hello]#

hello cgroup 节点默认启用了 OOM 终止程序,因此,当有进程尝试使用超过可用内存时会被立即终止。查询 memory.failcnt 可知,目前还没有进程内存达到过设定的最大内存限制值。

我们已经设定了 hello cgroup 节点可使用的最大内存为 100M ,此时新启动一个 bash 会话进程并将其移入到 hello cgroup 节点中:

[root@host hello]# /bin/bash
[root@host hello]# echo $$
4123
[root@host hello]# cat tasks
[root@host hello]# echo $$ > tasks
[root@host hello]# cat tasks
4123
4135
[root@host hello]# cat memory.usage_in_bytes
196608
[root@host hello]#

后续在此会话进程所创建的子进程都会加入到该 hello cgroup 节点中(例如 pid 4135 就是由于执行 cat 命令而创建的新进程,被系统自动加入到了 tasks 文件中)。

同样篇幅问题,剩下的 subsystem 也不在本文一一展示了。

其实到这里,我们已经通过 NameSpace 技术帮进程隔离出自己单独的空间,并使用 Cgroups 技术限制和监控这些空间的资源开销,这种特殊的进程就是容器的本质

4. Docker 对 Cgroups 和 Namespaces 的应用

当我们创建了一个 Docker Container 之后就可以查看这个 Container 所具有的 cgroups 和 namespaces 了。

  1. 查看 Container 的 ID(cfca1212d140)和 PID(2240)配置。

$ docker ps
CONTAINER ID   IMAGE                   COMMAND   CREATED         STATUS       PORTS     NAMES
cfca1212d140   centos:centos7.9.2009   "bash"    18 months ago   Up 2 hours             vim-ide

$ docker inspect --format='{{.State.Pid}}' cfca1212d140
2240
  1. 查看 Container 的 cgroups 配置。

$ ll /sys/fs/cgroup/memory/docker/
总用量 0
drwxr-xr-x. 2 root root 0 6月   2 03:40 cfca1212d1407a89632a439e974e246d1f6edd0bbef9079f06addf2613e1d46f

$ cat /sys/fs/cgroup/memory/docker/cfca1212d1407a89632a439e974e246d1f6edd0bbef9079f06addf2613e1d46f/cgroup.procs 
2240

$ cat /sys/fs/cgroup/memory/docker/cfca1212d1407a89632a439e974e246d1f6edd0bbef9079f06addf2613e1d46f/memory.limit_in_bytes
9223372036854771712
  1. 查看 Container 的 namespaces 配置。

$ ls -l --time-style='+' /proc/2240/ns
总用量 0
lrwxrwxrwx. 1 root root 0  ipc -> ipc:[4026532433]
lrwxrwxrwx. 1 root root 0  mnt -> mnt:[4026532431]
lrwxrwxrwx. 1 root root 0  net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0  pid -> pid:[4026532434]
lrwxrwxrwx. 1 root root 0  user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0  uts -> uts:[4026532432]

 

总结

其实容器的底层原理并不难,本质上就是一个特殊的进程,特殊在为其创建了 NameSpace 隔离运行环境,用 Cgroups 为其控制了资源开销,这些都是站在 Linux 操作系统的肩膀上实现的,包括 Docker 的镜像实现也是利用了 UnionFS 的分层联合技术。

参考:

  1. Linux Namespace和Cgroup  Linux Namespace和Cgroup - Linux程序员 - SegmentFault 思否

  2. linux namespace and cgroup linux namespace and cgroup - 掘金

  3. Linux Namespace 入门系列:Namespace API Linux Namespace 入门系列:Namespace API - 知乎

  4. Docker 工作原理及容器化简易指南 http://dockone.io/article/8788

  5. 昨天有读者说他不会 docker,今天就给你肝出来了 https://mp.weixin.qq.com/s?__biz=MzU2NDg0OTgyMA==&mid=2247492639&idx=1&sn=ac607fbbe221e50c24db1a7ccb12518b

  6. Docker 系列:核心原理和实现(2) Docker 系列:核心原理和实现(2) | 郭宁的个人博客

  7. What Are Linux Namespaces and What Are They Used for? What Are Linux Namespaces and What Are They Used for?

  8. namespaces(7) — Linux manual page namespaces(7) - Linux manual page

  9. 极好 A deep dive into Linux namespaces A deep dive into Linux namespaces – Chord Simple

  10. Linux – cgroup & namespace Linux – cgroup & namespace – Benjr.tw

  11. cgroup_namespaces(7) — Linux manual pages  https://manpages.courier-mta.org/htmlman7/cgroup_namespaces.7.html
  12. Overview of Linux Namespace  Linux namespaces pid,network,mount,ipc,uts,user,cgroup

  13. Container and Cgroups https://enqueuezero.com/container-and-cgroups.html

  14. Everything You Need to Know about Linux Containers, Part I: Linux Control Groups and Process Isolation  Everything You Need to Know about Linux Containers, Part I: Linux Control Groups and Process Isolation | Linux Journal

  15. Anatomy of a Container: Namespaces, cgroups & Some Filesystem Magic - LinuxConf https://www.slideshare.net/jpetazzo/anatomy-of-a-container-namespaces-cgroups-some-filesystem-magic-linuxcon

  16. What even is a container: namespaces and cgroups What even is a container: namespaces and cgroups

  17. https://events.static.linuxfound.org/sites/events/files/slides/cgroup_and_namespaces.pdf
  18. Resource management: Linux kernel Namespaces and cgroups  https://sites.cs.ucsb.edu/~rich/class/cs293b-cloud/papers/lxc-namespace.pdf,http://www.haifux.org/lectures/299/netLec7.pdf
  19. #14 - Introduction to Linux Control Groups (Cgroups)

    Introduction to Linux Control Groups (Cgroups)

  20. Control Group v2 https://www.kernel.org/doc/Documentation/cgroup-v2.txt
    
  21. Docker 背后的内核知识——cgroups 资源限制 Docker背后的内核知识——cgroups资源限制_语言 & 开发_孙健波_InfoQ精选文章

  22. 在Linux中使用namespace和cgroup实现进程的资源隔离和限制 在Linux中使用namespace和cgroup实现进程的资源隔离和限制 | 滩之南

  23. CGroup Namespaces https://lwn.net/Articles/618873/

  24. CGroup 介绍、应用实例及原理描述 IBM Developer

  25. Restricting process CPU usage using nice, cpulimit, and cgroups Restricting process CPU usage using nice, cpulimit, and cgroups | Scout APM Blog

  26. Docker DCA – Linux Namespaces and cgroups

    Docker DCA - Linux Namespaces and cgroups - buildVirtual
  27. Demystifying Containers - Part I: Kernel Space https://medium.com/@saschagrunert/demystifying-containers-part-i-kernel-space-2c53d6979504

  28. Containers: Namespaces and Dockers http://www.i3s.unice.fr/~urvoy/docs/VICC/1_2_vicc.pdf
  29. Docker Namespace and Cgroups https://medium.com/@kasunmaduraeng/docker-namespace-and-cgroups-dece27c209c7

  30. Understanding Linux Container Scheduling  Understanding Linux Container Scheduling — Squarespace / Engineering

  31. Linux Container Primitives: PID and Network Namespaces - SCHUTZWERK Linux Container Primitives: PID and Network Namespaces - SCHUTZWERK
  32. Containers: cgroups, Linux kernel namespaces, ufs, Docker, and intro to Kubernetes pods Containers: cgroups, Linux kernel namespaces, ufs, Docker, and intro to Kubernetes pods - IBM MediaCenter

  33. Container Creation Using Namespaces and Bash Container Creation Using Namespaces and Bash | Nicolas Mesa

  34. Linux containers in 500 lines of code Linux containers in 500 lines of code

  35. Linux Container Primitives: cgroups, namespaces, and more! https://www.youtube.com/watch?v=x1npPrzyKfs

  36. Container Creation Using Namespaces and Bash  Container Creation Using Namespaces and Bash | Nicolas Mesa

  37. Containers from Scratch Containers from Scratch | posts

  38. Hands on Linux sandbox with namespaces and cgroups https://blogs.rdoproject.org/2015/08/hands-on-linux-sandbox-with-namespaces-and-cgroups/

  39. How to run Docker containers using common Linux tools (without Docker)  unshare – I Learned How To…

  40. Understanding containers (Part 1/3) Understanding containers (Part 1/3) – Bibi's blog

  41. Docker 学习笔记10 容器技术原理 PID Namespace Docker 学习笔记10 容器技术原理 PID Namespace | 码农家园

  42. Understanding Linux containers  Containers explained: What they are and why you should care

  43. runc source code——bootstrap分析2 runc source code——bootstrap分析2 – freesky-edward

  44. Demystifying Containers – Part I: Kernel Space Demystifying Containers - Part I: Kernel Space | SUSE Communities

  45. Linux Namespaces  Linux Namespaces

  46. Containerization Mechanisms: Namespaces Containerization Mechanisms: Namespaces - Selectel Blog

  47. Runc 容器初始化和容器逃逸 Runc 容器初始化和容器逃逸_Linux云计算网络-商业新知

  48. chroot, cgroups and namespaces — An overview https://itnext.io/chroot-cgroups-and-namespaces-an-overview-37124d995e3d

  49. Everything You Need to Know about Linux Containers, Part I: Linux Control Groups and Process Isolation Everything You Need to Know about Linux Containers, Part I: Linux Control Groups and Process Isolation | Linux Journal

  50. Namespaces and Cgroups – the basis of Linux Containers  Meetup - We are what we do
  51. LXC, Cgroups and Advanced Linux Container Technology Lecture http://www.novell.com/feeds/nih/wp-content/uploads/2012/05/SUS15_lec.pdf
  52. Linux Containers https://www.cl.cam.ac.uk/~lc525/files/Linux_Containers.pdf
  53. Linux Container Primitives: cgroups, namespaces, and more! (LinuxFest Northwest 2019)

    https://speakerdeck.com/samuelkarp/linux-container-primitives-cgroups-namespaces-and-more-linuxfest-northwest-2019

  54. Container Performance Analysis  http://www.brendangregg.com/Slides/LISA2017_Container_Performance_Analysis.pdf

  55. Understanding cgroups https://www.grant.pizza/blog/understanding-cgroups/

  56. cgroups  cgroups - ArchWiki

  57. Understanding Linux Container Scheduling Understanding Linux Container Scheduling — Squarespace / Engineering

  58. Understanding Linux Container Scheduling Understanding Linux Container Scheduling — Kevin Lynch

  59. Linux Kernel Isolation Features From the Frog's mouth - JFrog Blog

  60. Introduction to Linux Containers Introduction to Linux Containers | CloudNativeLab

  61. All things Linux containers containerz.info

  62. src/syscall/exec_linux.go - The Go Programming Language https://golang.org/src/syscall/exec_linux.go

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/101137.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何在 Vue TypeScript 项目使用 emits 事件

Vue是构建出色的Web应用程序的最灵活、灵活和强大的JavaScript框架之一。Vue中最重要的概念和关键特性之一是能够促进应用程序组件之间的通信。让我们深入探讨一下Vue中的“emits”概念&#xff0c;并了解它们如何以流畅和无缝的方式实现父子组件之间的通信。 Vue中的emits是什…

工控上位机程序为什么只能用C语言?

工控上位机程序并不只能用C#开发&#xff0c;实际上在工业自动化领域中&#xff0c;常见的上位机开发语言包括但不限于以下几种&#xff1a;C#: C#是一种常用的编程语言&#xff0c;在工控领域中被广泛使用。它具有良好的面向对象特性和丰富的类库支持&#xff0c;可以实现高性…

艾昆纬携手亚马逊云科技赋能生物医药企业,加速武田数字化转型

IQVIA艾昆纬是全球以及中国医疗健康服务领域的领导者&#xff0c;利用其数据及分析&#xff0c;前沿数字化技术和医疗领域的专业知识&#xff0c;智能连接医疗生态的各个环节。过去几年&#xff0c;IQVIA艾昆纬所服务的生物医药行业客户武田中国也迎来了数字化转型的高潮。 武田…

习题练习 C语言(暑期第三弹)

自我小提升&#xff01; 前言一、存储地址二、逗号表达式三、除自身以外数组的乘积四、字节与二进制五、符号计算六、不用加减乘除做加法七、unsigned判断八、移位计算九、sizeof宏十、移位计算十一、移位计算十二、优先级判断十三、单词倒排总结 前言 重要的事说三遍&#xf…

工厂方法模式的概述和使用

目录 一、工厂方法模式概述1. 定义2. 使用动机 二、工厂方法模式结构1. 模式结构2. 时序图 三、工厂方法模式的使用实例四、工厂方法模式的优缺点五、工厂方法模式在Java中应用 原文链接 一、工厂方法模式概述 1. 定义 工厂方法模式(Factory Method Pattern)又称为工厂模式&…

python3+requests:接口自动化测试(二)

前言&#xff1a;上篇文章python3requestsunittest&#xff1a;接口自动化测试&#xff08;一&#xff09;&#xff1a;已经介绍了基于unittest框架的实现接口自动化&#xff0c;但是也存在一些问题&#xff0c;比如最明显的测试数据和业务没有区分开&#xff0c;接口用例不便于…

禅道项目管理系统 - 操作使用 (2023版)

1. 部门-用户-权限 新增部门 新增用户 设置权限 2. 项目集创建 项目集 - 添加项目集 3. 产品线创建 产品 - 产品线 4. 产品创建 产品 - 产品列表 - 添加产品 5. 产品计划创建 产品 - xx产品 - 计划 - 创建计划 我这里创建3个计划 (一期, 二期, 三期) 6. 研发需求 - 创建模块…

设计模式-中介者模式

文章目录 一、前言二、中介者模式1、定义2、未使用/使用中介者模式对比2.1、未使用中介者模式&#xff1a;2.2、使用中介者模式&#xff1a; 3、角色分析3.1、中介者&#xff08;Mediator&#xff09;&#xff1a;3.2、同事&#xff08;Colleague&#xff09;&#xff1a;3.3、…

Springboot2.0 上传图片 jar包导出启动(第二章)

目录 一&#xff0c;目录文件结构讲解二&#xff0c;文件上传实战三&#xff0c;jar包方式运行web项目的文件上传和访问处理&#xff08;核心知识&#xff09;最后 一&#xff0c;目录文件结构讲解 简介&#xff1a;讲解SpringBoot目录文件结构和官方推荐的目录规范 1、目录讲解…

SAP_ABAP_接口技术_RFC远程函数实践总结

SAP ABAP顾问能力模型梳理_企业数字化建设者的博客-CSDN博客SAP Abap顾问能力模型&#xff0c;ALV/REPORT|SMARTFROM|SCREEN|OLE|BAPI|BDC|PI|IDOC|RFC|API|WEBSERVICE|Enhancement|UserExits|Badi|Debughttps://blog.csdn.net/java_zhong1990/article/details/132469977 SAP接…

RabbitMQ-常用命令

RabbitMQ常用命令 3.1 启动停止rabbitMQ命令 # 前台启动Erlang VM 和 RabbitMQ 当窗口关闭或者ctrlc时&#xff0c;使退出了。 rabbitmq-server# 使用系统命令启动 systemctl start rabbitmq-server# 后台启动 rabbitmq-server -detached# 停止rabbitMQ和Erlang VM rabbitmq-…

【Python基础教程】快速找到多个字典中的公共键(key)的方法

前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 方法一&#xff1a;for in循环 from random import randint, samplea1 {k: randint(1, 4) for k in abcdefg} a2 {k: randint(1, 4) for k in abc123456…

【OpenCV入门】第七部分——图像的几何变换

文章结构 缩放dsize参数实现缩放fx参数和fy参数实现缩放 翻转仿射变换平移旋转倾斜 透视cmath模块 缩放 通过resize()方法可以随意更改图像的大小比例&#xff1a; dst cv2.resize(src, dsize, fx, fy, interpolation)src&#xff1a; 原始图像dsize&#xff1a; 输出图像的…

代码随想录—力扣算法题:19删除链表的倒数第N个节点.Java版(示例代码与导图详解)

19.删除链表的倒数第N个节点 力扣题目链接 更多内容可点击此处跳转到代码随想录&#xff0c;看原版文件 给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 进阶&#xff1a;你能尝试使用一趟扫描实现吗&#xff1f; 示例 1&#xff1…

六、vim编辑器的使用

1、编辑器 (1)编辑器就是一款软件。 (2)作用就是用来编辑文件&#xff0c;譬如编辑文字、编写代码。 (3)Windows中常用的编辑器&#xff0c;有自带的有记事本(notepad)&#xff0c;比较好用的notepad、VSCode等。 (4)Linux中常用的编辑器&#xff0c;自带的最古老的vi&…

UG\NX CAM二次开发 插入工序 UF_OPER_create

文章作者:代工 来源网站:NX CAM二次开发专栏 简介: UG\NX CAM二次开发 插入工序 UF_OPER_create 效果: 代码: void MyClass::do_it() {tag_t setup_tag=NULL_TAG;UF_SETUP_ask_setup(&setup_tag);if (setup_tag==NULL_TAG){uc1601("请先初始化加工环境…

通过类定义一个网络

import torch from torch import nnx torch.ones(2,10)class MLP(nn.Module):def __init__(self):super().__init__()self.out nn.Linear(10, 1)def forward(self,x):return self.out(x) 1. 代码解析 如何定义一个类&#xff1f;self 又是什么东西&#xff1f;类是如何继承基…

Doris架构中包含哪些技术?

Doris主要整合了Google Mesa(数据模型)&#xff0c;Apache Impala(MPP Query Engine)和Apache ORCFile (存储格式&#xff0c;编码和压缩)的技术。 为什么要将这三种技术整合? Mesa可以满足我们许多存储需求的需求&#xff0c;但是Mesa本身不提供SQL查询引擎。 Impala是一个…

开发指导—利用CSS动画实现HarmonyOS动效(一)

注&#xff1a;本文内容分享转载自 HarmonyOS Developer 官网文档 一. CSS 语法参考 CSS 是描述 HML 页面结构的样式语言。所有组件均存在系统默认样式&#xff0c;也可在页面 CSS 样式文件中对组件、页面自定义不同的样式。请参考通用样式了解兼容 JS 的类 Web 开发范式支持的…

TCP协议报文

前言 TCP/IP协议簇——打开虚拟世界大门中&#xff0c;已经给大家大致介绍了TCP/IP协议簇的分层。 TCP (Transmission Control Protocol)传输控制协议&#xff0c;在TCP/IP协议簇中&#xff0c;处于传输层。是为了在不可靠的互联网络&#xff08;IP协议&#xff09;中&#x…